sysrepo - 1.4.2 笔记
Sysrepo
是 Linux/Unix
系统下一个基于 YANG
模型的配置和操作数据库,为应用程序提供统一的操作数据的接口。应用程序使用 YANG
模型来建模,通过利用 YANG
模型完成数据合法性的检查,保证的风格的一致,不需要应用程序直接操作配置文件的一种数据管理方式。
sysrepo
只是一个库,不是一个独立的进程Yang
模型区分,这就可能造成许多严重的后果,例如,允许同时使用不同的模型进行工作,这将可 导致数据访问时异常。IPC
中使用共享内存的方式,取代了之前的 UNIX中进程间通信的方式,这样更高效,性能更优,扩展性更强sysrepo
中几乎不存在 CPU 时间浪费,没有活动等待或者定期检查poll/select
到自动线程处理sysrepo
操作期间可以修改 Yang
模型sysrepo
的主要功能是使用 YANG
模型对数据进行操作并订阅各种事件。但是,在执行任何操作时,都需要创建会话,连接会话,并要 install
所支持的各类 Yang
模型。假如设置了日志操作记录,sysrepo
在运行时,也可以保留它的行为记录。Yang
的 xpath
来修改与获取数据,所以要求了解 xpath
的基础知识。sysrepo
可以充当更智能的配置文件,从而保证配置的可恢复性。Rpc/Action/Notify
的订阅,这样可以通过执行特别的 Rpc
,就可以分别向其他 sysrepo
客户端通知各种生成的事件。应用程序可以通过两种方法来访问 sysrepo
,一种是直接的方法,即当应用程序需要配置数据或者执行相应的 callback
来响应配置变化时,可以通过 sysrepo
自带的应用程序来触发用 sysrepo
的功能函数来实现。这种方法一般用于开发人员自测或验证某个模块时使用;另一种是间接的方法,即应用程序通过创建 Deamon
进程的方法,该方法是通过将对 sysrepo
的调用转化为对应用程序的特定操作,该方法也最容易扩展,也无需为了使用 sysrepo
数据库而做相应的更改。如果有多个类似的 Deamon
进程,可以将这些进程合成一个 plugind
,最后由一个进程统一纳管。可扩展性得到大大的提高。间接方法的使用如图所示:
数据库结构大多是遵循 NMDA
(网络管理数据存储区体)所定义的体系架构。sysrepo
同样也不例外,sysrepo
中定义了四类数据库,分别是 startup
,running
,candidate
和 operational
。
startup
库,是 sysrepo
中唯一的持久性数据存储库,它包含设备启动时的配置文件,系统启动后创建的第一个 sysrepo
连接(共享内存)时,会将配置文件从 startup
库 copy
到 running
库;running
数据库,是保存当前所运行时系统配置,当一个配置发生变化时并且设备需要重新配置时, running
数据库需要修改。系统重启时不会存在,如果需要,可以将配置 copy
到 startup
库。candidate
数据库,候选库,顾名思义,它是一个准备配置的数据但又不影响实际设备使用。虽然该库中的数据不限制设备的正常使用,可以不必严格按照 NETCONF协议的定义,但也是需要遵循一般的数据存储规则。该库正常是无效的,实际使用时,需要将该库 mirror
到 running
,由 running
完成修改和配置下发,最后通过 sr_copy_config()
, 将 candidate
库重置。整个会话的过程中可能需要相应的 lock
操作,来保证操作的一致与完整性。operational
库,维护当前使用的配置,并且该库只可读。它通常与对应的 running
库有所不同,而且,只包含任何状态数据结点。在默认的情况下,该库是空的,对于用户来说,全部的订阅数据和操作数据都存储于 operational
库中。并且 Notificationg RPC/Action
数据的校验都是在 operational
库是完成。*_subscribe()
函数来做相应的订阅。订阅在原则上是将全部的的事件一并处理,应用程序也可以将根据不同的事件类型拆分成多个不同的订阅,用于保证事件的并发处理。sysrepo
做统一管理。sysrepo
创建一个单独的线程来捕获各种订阅事件的发生,然后通过订阅所注册的回调函数不处理它们。sysrepo
提供两个独立的,非常实用的程序。方便开发者便捷地使用 sysrepo
来开发与调试自己的应用。
sysrepoctl
,它用于列出,安装,卸载或更新 sysrepo
模块,也能用于修改一个 sysrepo
模块的特性,权限等。开发过程中经常使用的命令如下
1 | 列出全部已经安装在 sysrepo 中的 Yang 模块,并包含模块的基本信息 |
1 | 安装指定 Yang 模型 |
1 | 卸载已安装的 Yang 模型 |
1 | 修改 Yang 模型,常用的是设置模型支持的特性 |
1 | 更新 Yang 模型,如果已安装的 Yang 模型有更新,可以执行该命令 |
更多 sysrepoctl
的使用,请参考 sysrepoctl -h
。
sysrepocfg
是用于 importing
,exporting
,editing
,replacing
配置到指定的数据库中。命令默认是操作 running
库,也支持多种数据格式,json
, xml
, lyb
,除非通过 –format
特定指出,默认的采用 xml
格式。常用的命令如下:
1 | 导入一个配置 |
1 | 导出一个配置 |
1 | 编辑或修改配置文件,应用到指定的数据库 |
1 | 发一个 RPC 请求,RPC 返回的结果直接输出于终端 |
更多 sysrepocfg
的使用,请参考 sysrepocfg -h
。
应用程序通过将对 sysrepo
的调用通过 sysrepo
提供的相应的 API接口访问方法,称为 syrepo
的间接访问方法。该方法是应用程序通过创建 Deamon进程,通过 IPC Shm
机制与 sysrepo
通信。可以做到对 sysrepo
的即插即用,最后由 sysrepo
纳管,这就是 Plugind
,命名为 sysrepo-plugind
。要快速的使用 sysrepo
,并快速开发出适配于 sysrepo
的插件,就要先了解 sysrepo-plugind
的实现原理与机制,就需要先从实现 sysrepo-plugind
的源码处着手。sysrepo
源码路径:git clone https://github.com/sysrepo/sysrepo.git
。 Sysrepo-plugind
实现的路径为 sysrepo/src/executables/sysrepo-plugind.c
。下面也就从该文件开始说。
1 | struct srpd_plugin_s { |
1 | int main(int argc, char** argv) |
1 | static int |
1 | // srpd_plugin_s 结构中定义了 init 的回调函数 |
1 | // srpd_plugin_s 结构中定义了 cleanup 的回调函数 |
整个 sysrepo-plugind.c
代码结构简单,注释丰富,没有使用复杂的语法,还是非常容易理解的。
开发者要开始使用 sysrepo
,首先必须创建一个连接。一个应用程序或者进程即使可以允许创建多个连接,但是一般情况只会创建一个连接。sysrepo
允许同时创建多个连接。简单的举个例子,通常情况下,sysrepo-plugin
在 init_cb
初始时就会创建一个连接,这是一个由 sysrepo-plugin
与 sysrepo
所创建的连接,只要发生异常不释放,该连接会一直存在整个 sysrepo-plugin
进程的生命周期,此外,例如用户通过 sysrepoctl -l |grep ***
看某个 Yang
模型是否已经加载,sysrepoctl
应用程序也创建一个短连接,该连接在命令执行结束后立即释放,假如是极端修改,不释放该连接,再使用 sysrepocfg
来配置 runing
库,这时有 3 个与 sysrepo
连接。并且 3 个连接不干扰,也不影响 sysrepo
的正常工作。
而会话,是建立在连接之下,一个连接下可以创建多个会话,每个会话都有一个唯一的标识,每个会话总是可以选择一个可随时更改的数据库,使用些会话的所有 API 调用都将在该数据库下操作。
连接与会话的关系如下所示,可能不是特别准备,但大概就是这个意思。
connection
的数据结构主要是存储 sysrepo
连接与 Libyang
的上下文,该连接所创建的共享内存结构。数据结构定义如下
1 |
|
cache
需要特别说明:如果一个会话工作在 running
的数据库下操作,并且该会话的连接使能 cache
功能,则不会每次都从 sysrepo
中加载数据,可以从 cache
中复制数据,这样,可以大幅度提高 sysrepo
的处理性能。
session
的主要数据结构
1 | /** |
从 session
结构中主要是用于该次 session
的连接,该次 session
要连接的数据库类型(4种,runing
, startup
, candidate
, operation
),以及重中之重的 sr_subscription_ctx_t **subscriptions
, sysrepo
的所支持操作的订阅都在该结构中定义,不多说,直接看数据结构定义:
1 | /** |
1 | /*功能:连接sysrepo数据库 |
1 | /* 功能:清除与释放由sr_connect分配的的连接上下文, |
1 | /*功能:开始一个新的session |
1 | /* 功能:停止当前session并且释放与该session所维系的全部资源 |
连接与会话核心处就是这 4 个 API 函数, 其它与连接与会话有关的 API 都是对相关的补充,想要进一步了解的.请阅读源码.
接下来会分析 sysrepo
的共享内存机制. SHM
机制是新 sysrepo
的核心,需要好好说道说道.
sysrepo0.X.X
版本使用的进程间通信的机制,在实际的使用过程中,出现了诸如数据不同步、数据处理TimeOut
、完成一次 Get
请求时,但实际处理的请求会较多,导致性能与规格上不去的各类问题。sysrepo-devel
分支开始引入共享机制后,合入到 sysrepo
的 Master
分支,也就是现在的 sysrepo1.X.X
版本。
简单说一说什么是共享内存,共享内存就是允许两个或多个进程共享一定的存储区,说白了,就是两个进程访问同一块内存区域,当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改,所以数据不需要在客户机和服务器端之间复制,数据直接写到内存,不用若干次数据拷贝,是一种最快的 IPC
。原理图如下所示,需要注意的是,共享内存本向并没有任何的同步与互斥机制,所以必须使用信号量来实现对共享内存的存取的同步。其它有关的共享内存的概念使用,网上有很多,可自行查阅理解。本处这分析与 sysrepo
相关的共享内存机制的使用。
1 | /** |
此添加 shm main
的入口代码,将全部模块以 lydmod
数据形式添加到 main SHM
中。参考前一章的 sr_connect
函数,这就是将在与 sysrepo
连接时,会将全部模块的加载到共享内存中。
1 | sr_error_info_t * |
共享内存间在初始操作,包括信号的创建与初始化,也是在 sr_connet
函数中处理。``sr_connet是
plugind与
sysrepo的连接入口,
SHM是在入口中初始的一种机制,用来保证
sysrepo与
plugind` 的通信高效,快速。
先用 sysrepo
共享内存机制为后面的各类订阅打个底。先了解一下 sysrepo
的共享内存机理的实现。
libyang – GitHub
netopeer2 – GitHub
sysrepo – GitHub
pyang – GitHub
libyang – Doc
libnetconf2 – Doc
sysrepo – Doc
pyang – Doc
XPath 教程 – RUNOOB.COM
XPath教程 – 易百教程
]]>netopeer2 + sysrepo研究总结
sysrepo简单使用
第三章 sysrepo-plugind源码分析
1 | sudo yum -y update |
1 | git clone https://github.com/CESNET/libyang.git |
Documentation:
可以使用 Doxygen 工具直接从源代码生成库文档:
1 | make doc |
更改扩展插件目录:
至于 YANG 扩展,libyang 允许加载扩展插件。默认情况下,存储插件的目录是 LIBDIR/libyang。要更改它,使用下面的 cmake 选项,该选项的值指定所需的目录:
1 | cmake -DPLUGINS_DIR:PATH=`pwd`"/src/extensions/" .. |
目录路径也可以通过环境变量文件改变运行时,例如:
1 | LIBYANG_EXTENSIONS_PLUGINS_DIR=`pwd`/my/relative/path yanglint |
yanglint:
Libyang 项目包括一个名为 yanglint (1)的功能丰富的工具,用于验证和转换模式和 YANG 模型数据。源代码位于/tools/lint,可用于探索应用程序如何使用 libyang 库。Yanglint (1)二进制文件及其手册页与库本身一起安装。
There is also README describing some examples of using yanglint
.
还有自述文件,描述了使用阳光棉的一些例子。
Libyang 通过插件机制支持 YANG 扩展。一些插件(针对 NACM 或 Metadata)可以开箱即用,并与 libyang 一起安装。但是,如果没有安装 libyang,而是从构建目录中使用 yanglint (1) ,那么这些插件就不可用。有两种选择:
1 | make install |
LIBYANG_EXTENSIONS_PLUGINS_DIR
包含路径到构建的扩展插件(从构建目录 ./src/extensions
)1 | LIBYANG_EXTENSIONS_PLUGINS_DIR="`pwd`/src/extensions" ./yanglint |
依赖:
1 | git clone http://git.libssh.org/projects/libssh.git |
1 | git clone https://github.com/sysrepo/sysrepo.git |
1 | git clone https://github.com/CESNET/Netopeer2.git |
libyang doc
libyang is a library implementing processing of the YANG schemas and data modeled by the YANG language. The library is implemented in C for GNU/Linux and provides C API.
Libyang 是一个用 YANG 语言实现 YANG 模式和数据处理的库。该库是用 c 语言为 GNU/Linux 实现的,并提供了 c API。
当前的实现包括 YANG 1.0 (RFC 6020) 和 YANG 1.1 (RFC 7950)。
libnetconf2 is a NETCONF library in C handling NETCONF authentication and all NETCONF RPC communication both server and client-side. Note that NETCONF datastore implementation is not a part of this library. The library supports both NETCONF 1.0 (RFC 4741) as well as NETCONF 1.1 (RFC 6241).
libnetconf2 是一个 C 语言的 NETCONF 库,处理 NETCONF 认证和所有 NETCONF RPC 通信服务器和客户端。注意,NETCONF 数据存储实现不是这个库的一部分。这个库同时支持 NETCONF 1.0 (RFC 4741) 和 NETCONF 1.1 (RFC 6241)。
YANG
是最初设计用于为NETCONF
协议建模的语言。 YANG
模块定义了可用于基于NETCONF
的操作(包括配置,状态数据,RPC
和通知)的数据层次结构。这允许在NETCONF
客户端和服务器之间发送的所有数据的完整描述。虽然不在本规范的范围之内,但是也可以使用除NETCONF
以外的协议。
YANG
将数据的分层组织模型化为一个树,其中每个节点都有一个名称,或者一个值或一组子节点。YANG
提供了对节点的清晰简洁的描述,以及这些节点之间的交互。
YANG
将数据模型组织成模块和子模块。模块可以从其他外部模块导入定义,并可以包含子模块的定义。可以增加层次结构,允许一个模块将数据节点添加到另一个模块中定义的层次结构中。这种增加可以是有条件的,只有在满足某些条件的情况下才会出现新的节点。
一个模块包含三种类型的语句:
模块头(module header
)语句,
“修订”(revision
)语句
定义(definition
)语句。
模块头部语句描述模块并提供关于模块本身的信息,“修订”语句提供关于模块历史的信息,定义语句是定义数据模型的模块的主体。
数据模型(data model
):
数据节点(data node
):
container
,leaf
,leaf-list
,list
,anydata
和anyxml
之一。数据树(data tree
):
YANG
建模的任何数据的实例化树,例如配置数据,状态数据,组合配置和状态数据,RPC
或操作输入,RPC
或操作输出或通知。叶节点(leaf
):
叶列表(leaf-list
):
RPC
操作(RPC operation
):
模式节点(schema node
):
action
,container
,leaf
,leaf-list
,list
,choice
,case
,rpc
,input
,output
,notification
,anydata
和anyxml
中的一个。模式树(schema tree
):
叶节点(Leaf Nodes)
“leaf
”语句用于在模式树中定义叶节点。 它需要一个参数,它是一个标识符,后面是一个包含详细叶子信息的子状态块。一个leaf node
包含且只包含一个value
,可以是数字或是字符串,具体是什么,看关键字”type”后面跟什么。leaf node
下面不能挂子节点。如:
1 | YANG Example: |
叶列表节点(Leaf-List Nodes)
可以认为Leaf-List Nodes表示的是一个“数组”,“数组”中的元素的值的type必须保持一致,可以有一系列同类型的值,而且不能重复。
1 | YANG Example: |
容器节点(Container Nodes)
一个容器用于分组子树中的相关节点。 一个容器只有子节点,没有值。 容器可以包含任何类型的任何数量的子节点(叶子,列表,容器,叶子列表,动作和通知)。只能装东西,本身不具有意义。
1 | YANG Example: |
列表节点(List Nodes)
列表定义了一系列列表条目。每个条目就像一个容器,如果它定义了任何关键的叶子(指明一个叶子为 key
),它就被其关键叶子的值唯一标识。列表可以定义多个关键叶子,并且可以包含任何类型的任何数量的子节点(包括树叶,列表,容器等)。实例化的时候,key
的值(也就是”name”的值)是必须不同的,其它的值(full-name/class)没有这个要求。
1 | YANG Example: |
libyang
是一个实现YANG
模式处理的library
和由YANG
语言建模的数据
主要特点:
YANG
格式解析(和验证) schema
YIN
格式解析(和验证) schema
XML
格式解析,验证和打印实例数据JSON
格式解析,验证和打印实例数据YANG
扩展和实例类型YANG
元数据XML
解析器过程:先创建一个context,解析yang文件生成schema tree(schema tree 相当于类型定义,指定生成的数据类型),然后根据文件读入,生成对应的data tree(相当于实例化的数据)
上下文概念允许调用者在具有不同 YANG
模式集的环境中工作,具体工作流程(代码过程)见下:
1 | ly_ctx_new() //创建新的上下文 |
对于context
,第一次请求module
的最新版本时,将正确搜索并加载该module
。但是,当第二次请求此module
(没有修订)时,将返回先前找到的module
。这样做的好处是不会反复搜索module
,但缺点是如果稍后可以使用module
的后续版本,则此context
不会使用它。
context
在内部保存所有model
及其 submodel
context
包括更有效地存储字符串的字典。大多数字符串在 schema
和 data tree
经常重复。因此,libyang
不会在每次出现时分配这些字符串,而是将它们存储为字典中的记录。
schema
是 YANG
数据模型在 libyang
中的内部表示,每个 schema
都与其 context
连接,并使用解析器函数加载,因此无法以编程方式创建(更改) schema
。在 libyang
中,schema
仅用于访问数据模型定义模式树节点能够保存调用者应用程序使用的私有对象(通过指向结构,函数,变量等的指针)
1 | lys_set_private() //将私有对象分配给特定节点 |
schema
解析器允许从特定格式读取 schema
。libyang
支持以下架构格式:YANG
,YIN
1 | //以lys_features_为前缀的函数组用于访问和操作模式的功能。 |
plugins
形式支持扩展和用户类型。无论何时创建上下文,都会从 plugins
目录 LIBDIR/libyang/
加载它们。扩展 plugins
目录路径(默认 LIBDIR/libyang/extensions/
)可以通过LIBYANG_EXTENSIONS_PLUGINS_DIR
环境变量更改,类似地通过用户类型目录(默认LIBDIR/libyang/user_types/
)更改 LIBYANG_USER_TYPES_PLUGINS_DIR
。请注意,不会删除不可用的plugins
,只会加载任何新 plugins
。另请注意,新 plugins
的可用性不会影响上下文中的当前 schema
,它们仅应用于新解析的 schema
。
1 | 扩展 plugins 目录路径 |
1 | ly_load_plugins() //手动刷新plugins列表 |
schema printing
允许以特定格式序列化模式 schema
的内部表示 ,包括:YING
,YANG
,Tree
每个节点的模块的简单树结构被打印为:
1 | <status> <flags> <name> <opts> <type> <if-features> |
data tree
中的所有 data
节点都和他们的 schema
节点相连
与 schema
解析器相反,如果根据 libyang 上下文中的 schema
,这样的空数据树是有效的,则 data
解析器也接受空输入数据。
在创建/插入节点时,该操作中的所有对象必须属于同一个上下文
创建数据:
根据节点名称或其父节点逐个添加节点
1 | lyd_new() |
使用简单的 XPath
寻址
1 | lyd_new_path() |
Sysrepo 是一个基于 yang 的 Unix/Linux 系统数据存储。使用 YANG 建模配置的应用程序可以使用 Sysrepo 进行管理。
申请使用 Sysrepo 的方法主要有两种。直接方法包括在需要配置数据时从应用程序本身调用 Sysrepo 函数,或者执行特定的回调以对配置更改做出反应。还可以实现一个独立的守护进程,将 Sysrepo 调用转换为应用程序特定的操作。对于现有的应用程序,这种间接方法通常比较容易使用,因为这样就不需要修改它们自己来利用 Sysrepo 数据存储,而代价是需要一个额外的中间进程(守护进程)。如果有几个这样的守护进程,它们可以作为插件编写,然后由一个进程管理。
Sysrepo 是用于 Unix/Linux 应用程序的基于 yang 的配置和操作状态数据存储。
Sysrepo是一个基于YANG模型的配置和操作数据库,为应用程序提供一致的操作数据的接口,解决了配置读写困难的问题。应用程序使用YANG模型来建模,这样就可以利用YANG模型完成数据合法性的检查,保证的风格的一致,不需要应用程序直接操作配置文件了。
目前,应用程序可以使用 sysrepo Client Library 的 c 语言 API 访问数据存储中的配置,但是对其他编程语言的支持也计划在以后使用(因为 sysrepo 使用 Google 协议缓冲作为数据存储和客户端库之间的接口,为任何支持 GPB 的编程语言编写本地客户端库是可能的)。
Sysrepo 可以很容易地与管理代理(如 NETCONF 或 RESTCONF 服务器)集成,使用应用程序用于访问其配置的相同的客户端库 API。到目前为止,sysrepo 已经与 Netopeer 2 NETCONF 服务器集成。这意味着使用 sysrepo 存储其配置的应用程序可以自动受益于通过 NETCONF 进行控制的能力。
SYSREPO数据库它提供了以下特性:
sysrepo 实际只是保存配置,并调用回调函数这两件事。
有一些二进制文件是严格可选的,因为它们只使用Sysrepo API。但是,它们对于一些常见任务可能很有用,通过包含它们,每个用户不必从头开始编写它们。
这个应用程序是一个简单的守护进程,它将所有可用的 Sysrepo 插件分组到一个单独的进程中。这个守护进程从插件路径目录加载插件,并支持一些选项, --verbosity
和 --debug
,以避免进入守护进程模式,并保持将所有消息打印到 stderr。
Plugin 是一个共享对象,它必须公开两个函数: sr_plugin_init_cb()
和 sr_plugin_cleanup_cb()
,这两个函数分别在 sysrepo-plugind 的开始和结束时被调用。初始化函数必须执行所有运行时任务,因为守护进程不会调用其他函数。它通常包括创建各种订阅,然后自己处理事件。清理通常会停止这些订阅。
插件路径,这是存储插件的唯一途径。默认路径可以在编译过程中修改(PLUGINS_PATH
选项) ,但是如果设置了 $SRPD_PLUGINS_PATH
环境变量,则总是会覆盖这个默认路径。
它是一个实用工具,可以更改模式(模块)。具体来说,它可以列出、安装、卸载或更新它们。此外,还可以更改模块的特性、重播支持和权限。重要的是要记住哪些操作是立即执行的,哪些操作是延期的(模式中的详细信息)。
-l,–list
所有当前安装的模块都列在一个简明的表格中,其中包含有关它们的基本信息。还有关于任何准备好的更改的信息。
1 | sysrepoctl --list |
-i,–install <path>
YANG 模块的安装只需要指定它们的路径(YANG 或者 YIN 格式)。
1 | sysrepoctl --install ~/Documents/modules/ietf-interfaces.yang |
-u,–uninstall <module>
要删除 YANG 模块,必须指定它的名称(而不是文件名)。所有可以移除的已安装模块都是通过 --list
打印的。
1 | sysrepoctl --uninstall ietf-interfaces |
-c,–change <module>
已安装的模块可以通过多种方式进行更改,可以选择性地组合成一个命令。
1 | sysrepoctl --change ietf-interfaces --(disable|enable)-feature |
然后,它们的重播支持(存储接收到的通知)可以被打开或关闭。
1 | sysrepoctl --change ietf-interfaces --replay on |
最后,可以调整文件系统权限。
1 | sysrepoctl --change ietf-interfaces --owner netconf --group netconf --permissions 660 |
-U,–update <path>
已安装的 YANG 模块可以更新为更新的版本。
1 | sysrepoctl --update ~/Documents/modules/ietf-netconf@2013-09-29.yang |
-C,–connection-count
获取当前连接的客户端的数量。可用于检查是否可以立即应用某些架构更改(如果没有连接)。
1 | sysrepoctl --connection-count |
这个二进制文件允许以多种方式处理配置,比如导入、导出、编辑和替换(从文件或数据存储中复制)。还可以发送 rpc/action
或通知。
所有操作都在 --datastore
(默认运行、启动或操作)上执行,或者只在特定的 --module
上执行,并以支持的 --format
(默认 xml、 json 或 lyb)处理数据。
-I, –import[=<file-path>]
为了导入配置,通常会提供一个文件。它的格式将根据扩展自动检测。如果不适用(或者从 STDIN 读取数据) ,可以手动确定。
1 | sysrepocfg --import=~/Documents/data/running.xml |
还可以导入模块的启动配置。
1 | sysrepocfg --import=~/Documents/data/ietf-interfaces_startup.json --datastore startup --module ietf-interfaces |
-X,–export[=<file-path>]
可以将导出的配置打印到文件中,也可以直接打印到 STDOUT。
1 | sysrepocfg --export --datastore operational |
此外,只能检索配置的特定部分
1 | sysrepocfg --export=ietf-interfaces_running.lyb --format lyb --module ietf-interfaces |
或者 XPath 选择。
1 | sysrepocfg --export --xpath /ietf-interfaces:interfaces/interface[name='eth0'] |
-E,–edit[=<file-path>/<editor>]
可以在文件中或使用任意文本编辑器提供要合并的数据(作为编辑应用)。另外,在更改运行的数据时,可以锁定数据存储。
1 | sysrepocfg --edit=candidate.xml --datastore candidate |
-R,–rpc[=<file-path>/<editor>]
也可以从文件或使用编辑器发送 RPC 或操作。任何输出都打印到 STDOUT。
1 | sysrepocfg --rpc=vim |
-N,–notification[=<file-path>/<editor>]
以类似的方式执行发送通知。
1 | sysrepocfg --notification=notif.xml |
-C,–copy-from <file-path>/<source-datastore>
此操作可以用文件或其他数据存储的内容替换模块或数据存储数据。
1 | sysrepocfg --copy-from=ietf-interfaces_startup.xml --module ietf-interfaces --datastore startup |
第二个命令实际上是 NETCONF <commit>
,因为它将候选数据存储复制到默认运行的数据存储中。
这部分教你如何编写一个简单的 YANG 模块,然后让 Sysrepo 以插件或独立守护程序的形式处理数据。在继续之前,最好至少对 Sysrepo 有一个基本的了解。
对于任何你想用 Sysrepo 管理的设备,你都需要一个设备的 YANG 模块。该语言非常丰富,几乎可以对任何系统进行描述。例如,一个烤箱将完全由 Sysrepo 管理。将介绍 YANG 的所有基本部分,即配置数据、状态数据、 rpc 和通知。
为了简化事情,我们的烤箱是一个便宜的型号,只有一个开关和滑块来设置温度。但是,它可以提供内部实际温度的信息,并且当内部温度达到设定温度时通知厨师。此外,生的食物可以预先准备好,如果有提示,烤箱可以自动将食物放入或取出。这样我们就得到了 YANG 模型:
1 | module oven { |
这里将一步一步地解释如何写一个合适的插件,将管理烤箱。所有代码片段都取自实际的实现。插件 API
初始化
在初始化函数中,通常必须初始化设备并创建对任何相关 YANG 节点的订阅。
1 | food_inside = 0; |
首先,肯定要通知烤箱其配置参数的任何变化,这样最容易订阅整个模块。设置标志 SR_SUBSCR_ENABLED,以便在 sysrepo-plugind 启动时,独立于烤箱(设备)的状态,将当前存储的配置应用于设备并保持一致性。另一个标志 SR_SUBSCR_DONE_ONLY 被使用,因此不会调用回调来验证任何挂起的更改。对于我们的示例,只要基于 YANG 限制的值有效,它就总是正确的。
还可以订阅任意的配置数据子树,但这个示例不需要这样做。
1 | rc = sr_module_change_subscribe(session, "oven", oven_config_change_cb, NULL, 0, |
然后,由于在 oven 模型中还有状态数据,将执行标记该插件为它们的(独占)提供者的订阅。当 Sysrepo 需要状态数据子树时,通常在客户机请求它们时调用它。
值得注意的是,使用了相同的订阅对象,因此必须指定标志 SR_SUBSCR_CTX_REUSE。
1 | rc = sr_dp_get_items_subscribe(session, "/oven:oven-state", oven_state_cb, NULL, SR_SUBSCR_CTX_REUSE, &subscription); |
最后,该插件还可以处理任何 RPC 调用,这些调用也需要副本。
1 | rc = sr_rpc_subscribe(session, "/oven:insert-food", oven_insert_food_cb, NULL, SR_SUBSCR_CTX_REUSE, &subscription); |
Sysrepo 为插件提供了能够以统一方式打印消息的宏,因此建议使用它们。
1 | SRP_LOG_DBGMSG("OVEN: Oven plugin initialized successfully."); |
一般格式为 SRP_LOG_(level)(MSG)
。消息的严重性是由 DBG、 VRB、 WRN 或 ERR 之一编写的,而不是(级别)。在示例中,由于没有指定其他变量参数,因此使用了后缀 MSG。如果有,则省略此后缀。参数与 printf ()
函数使用的参数相同。
至于清理,所执行的任务差别很大,并且取决于设备。但是,总是需要适当地终止 init 函数中的订阅,这是本示例中所需的惟一工作。
1 | sr_unsubscribe(subscription); |
为了简化代码,subscription 被定义为一个全局变量,但是也可以使用 private_data,例如,也可以使用应用程序需要的任何附加数据来存储它。之前分配的所有其他回调都可以在需要时使用相同的机制传递附加数据。
在示例中,它用 oven_config_change_cb()
订阅模块更改。这里看到的代码是实际代码的简化,但是更好地理解回调应该做什么。
1 | static int |
首先,可以观察到,事件变量被忽略。在我们的示例中,无论处理哪个事件,我们都执行相同的操作,由于订阅标志,它将不会是除 SR_EV_DONE 之外的任何事件。
然后,读取并应用所有相关的数据节点。这种方法是最简单的方法,不能总是使用,但在这种情况下可以使用,因为可以重新应用更改而不会产生任何效果。更详细的机制(返回更改)是使用 sr_get_changes_iter() 和提供的会话,从而只获得特定的更改值。
1 | static int |
状态数据回调是自我解释的。由于订阅只针对一个有两片叶子的容器,因此路径只能有一个值。创建相应的子元素。
1 | static int |
RPC 回调应该执行相应的 RPC。移除食物只能做到这一点。但是插入食物有一些定义的输入参数,所以它们需要被处理。同样,如果存在某些输出参数,则需要创建并返回这些参数,但这不是我们的情况。
通知的提供者不需要订阅任何东西,只需在发生通知时生成任何通知即可。
1 | rc = sr_event_notif_send(sess, "/oven:oven-ready", NULL, 0); |
从这个例子中可以看出,这是相当简单的。此外,如果有通知的任何子节点,则需要创建它们,然后将其传递给函数。
模型和完整的插件源可以在 sysrepo/examples/plugin
中找到。生成 Sysrepo 后,烤箱共享库存储在示例中,但不会自动安装。在安装和实际运行插件之前,最好仔细阅读源代码。它只是一个很小的文档化的文件,所以它不应该花费很长时间,而且人们应该了解实现的烤箱功能。此外,上面章节中所涉及的大多数信息只是对所有这些机制的基本和详细描述。
在考虑这个特定的插件之前,必须正确构建和安装 Sysrepo。完成之后,您必须首先安装模型,然后安装插件。为了安装模型,可以使用 sysrepoctl。然后,您必须将共享库放入 liboven。进入插件路径。
之后,您应该准备启动 sysrepo-plugind
,它将加载插件。如果您启用了调试消息,您应该会看到烤箱插件已成功初始化。
现在您可以自由地使用烤箱配置、rpc和通知了。它应该像YANG模型中描述的那样工作,以及人们期望烤箱如何工作。下面是一个用例:
作为第一步,使用 notif 订阅示例订阅烤箱通知
1 | notif_subscribe_example oven |
准备好待烤箱温度达到一定温度后放入的食物,稍后再进行配置。烤箱默认是关闭的。在NETCONF术语中,执行插入-食物 RPC。您可以使用 sysrepocfg 实现这一点
1 | sysrepocfg --rpc=vim |
和输入:
1 | <insert-food xmlns="urn:sysrepo:oven"> |
作为 RPC 内容。您应该会看到一些信息 sysrepo-plugind 输出。
现在你要打开烤箱,期待当它达到设定的温度时得到通知。同时,食物应该在那个时候插入。所以,你执行
1 | sysrepocfg --edit=vim --datastore running |
与内容
1 | <oven xmlns="urn:sysrepo:oven"> |
在~4秒后,你应该收到通知。您还可以验证是否一切正常
1 | sysrepocfg --export --xpath /oven:* |
食物应该放在烤箱里。
一旦你认为食物烤得恰到好处,就用另一个RPC移走它
1 | sysrepocfg --rpc=vim |
和
1 | <remove-food xmlns="urn:sysrepo:oven"/> |
如果您希望您的设备有一个独立的守护进程,它将作为独立的进程运行,而不使用 sysrepo-plugind,那么您不需要开发插件。拥有一个独立的守护进程实际上只有上一句中提到的几个区别。
至于代码本身,不需要特定的函数,因为代码将编译为可执行二进制文件而不是共享库。但是,如果要将插件转换为应用程序,没有什么可以阻止重用整个代码。
所需要的只是一个 main()
函数,该函数将在开始处调用 sr_plugin_init_cb()
并在终止之前调用sr_plugin_cleanup_cb()
。 此外,这些功能需要 Sysrepo 会话。 要创建一个,我们首先需要一个连接。 因此,使用 sr_connect()
创建连接,如果成功,则使用 sr_session_start()
创建会话。 现在,可以通过调用清除函数并释放会话和连接来调用 init 函数并在守护程序终止时正确地进行清理。 此外,在编译此类应用程序之前,必须更改打印宏,因为将不再有处理打印消息的主守护程序。 完成这些更改后,烤箱守护程序应已准备就绪。
下载:libyang release
1 | tar -zxvf libyang-1.0.184.tar.gz |
更改扩展插件目录
对于 YANG 扩展,libyang 允许加载扩展插件。 默认情况下,存储插件的目录是 libdir/libyang
。 要更改它,使用env环境更改
1 | LIBYANG_EXTENSIONS_PLUGINS_DIR=`pwd`/my/relative/path yanglint |
netopeer2 + sysrepo研究总结
sysrepo是一个数据库。可以用来保存可读可写的配置,例如 IP,netmask。你可以坐在家里,给远端的设备的sysrepo下修改配置命令。另外远端设备还有一些只读的状态,也可以读回来,例如温度。
订阅实际上就是回调函数。
例如我们的程序告诉sysrepo,我们要订阅 /net/eth0/ip 这个 xml 地址,当有人发消息给 sysrepo,写这个路径时,sysrepo就会告诉我们这个 xml 发生变化了,我们就执行实际的操作。
修改配置用写xml路径的方法, 例如写xml路径/net/eth0/ip,那么sysrepo就会把修改的信息写到此路径中,保存到sysrepo的数据库中。
但是sysrepo并没有去做修改ip的实际工作,这个实际工作是怎么完成的呢?sysrepo提供了一个回调函数,我们只要把这个回调函数写好就行。当写xml路径时,sysrepo就会调用回调函数,完成实际的工作。sysrepo实际只是保存配置,并调用回调函数这两件事。
完成一个修改配置的操作,共需要3个程序,一个是发命令的程序,一个收命令的sysrepo,另外我们自己还要写个程序,接收sysrepo发过来的修改配置的路径,我们自己的程序发现是要修改ip的路径,那么就改ip了。
和 2 类似,我们的程序收到读某个路径的消息,就把数据写到指定路径中就可以
1 | git clone https://github.com/sysrepo/sysrepo.git |
Sysrepo/Netopeer2 tools and programs:
多数linux的应用程序需要有配置,配置文件的保存和读写通常的实现方式是通过操作文件来完成的。各应用程序都自定配置文件的格式,格式风格存在诸多差异。
Sysrepo是一个基于YANG模型的配置和操作数据库,为应用程序提供一致的操作数据的接口,解决了配置读写困难的问题。应用程序使用YANG模型来建模,这样就可以利用YANG模型完成数据合法性的检查,保证的风格的一致,不需要应用程序直接操作配置文件了。
SYSREPO数据库它提供了以下特性:
sysrepo 提供两个独立的,非常实用的程序。方便开发者便捷地使用 sysrepo 来开发与调试自己的应用。
sysrepoctl,它用于列出,安装,卸载或更新 sysrepo 模块,也能用于修改一个 sysrepo 模块的特性,权限等。开发过程中经常使用的命令如下
1 | sysrepoctl -l, --list |
1 | sysrepoctl -i, --install |
1 | sysrepoctl -u, --uninstall |
1 | sysrepoctl -c, --chang |
1 | sysrepoctl -U, --update |
sysrepocfg 是用于 importing,exporting,exporting,replacing 配置到指定的数据库中。命令默认是操作running 库,也支持多种数据格式:json, xml, lyb,除非通过 –format
特定指出,默认的采用 xml 格式。常用的命令如下:
1 | sysrepocfg -I, --import[=] |
1 | sysrepocfg -X, --export[=] |
1 | sysrepocfg -E, --edit[=/] |
1 | sysrepocfg -R, --rpc[=/] |
更多sysrepocfg的使用,请参考sysrepocfg -h。
1 | cp /tmp/o-ran-software-management.so /usr/lib/sysrepo/plugins/ |
o-hub software download xml fotmat:
1 | /* sftp://<username>@<host>[:<port>]/path */ |
o-hub software install xml fotmat:
1 | <software-install xmlns="urn:o-ran:software-management:1.0"> |
o-hub software activate xml fotmat:
1 | <software-activate xmlns="urn:o-ran:software-management:1.0"> |
1 | mpi_read 0x2 0x1000 0x20 # 读2次 |
1 | mpi_read 0x2 0x1040 0x20 # 读2次 |
1 | mpi_read 0x2 0x0000 0x20 # 读2次 |
参数说明:
一级HUB BU 到 RU 时延值:
环境:BU –> HUB –> RU
1) BU –> HUB T12
2) TBdelayDL –> HUB上联口到对应 RU接HUB下联口的时延值,单位Cycle
3)HUB –> RU T12_1
4) T2a RU 上报,单位Cycle
下行 BU 到 RU 时延值:
二级HUB BU 到 RU 时延值:
环境:BU –> HUB –> HUB –> RU
1) BU –> HUB T12
2) TBdelayDL –> 一级HUB 上联口到 一级HUB 级联口时延值,单位Cycle
3) HUB –> HUB T12_1
4)TBdelayDL_1 –> 二级HUB上联口到对应 RU接 二级HUB下联口的时延值,单位Cycle
5)HUB –> RU T12_2
6) T2a RU 上报,单位Cycle
下行 BU 到 RU时延值:
1 | snmptrap -v1 -c public 192.168.2.124 .1.3.6.1.4.1.1 192.168.2.125 6 10 100 1.3.6.1.9.9.44.1.2.1 i 12 |
libyang – GitHub
netopeer2 – GitHub
sysrepo – GitHub
libyang – Doc
libnetconf2 – Doc
sysrepo – Doc
XPath 教程 – RUNOOB.COM
XPath教程 – 易百教程
]]>netopeer2 + sysrepo研究总结
sysrepo简单使用
第三章 sysrepo-plugind源码分析
随着SDN的大热,一个诞生了十年之久的协议焕发了第二春,它就是NETCONF协议。如果你在两年前去搜索NETCONF协议,基本得到的信息都是“这个协议是一个网管协议,主要目的是弥补SNMP协议的不足,希望可以取代SNMP协议”。SNMP有哪些不足,而NETCONF是否真的能够弥补,这都不是重点,重点是NETCONF诞生至今SNMP依旧活的好好的。所以如果我们还是把NETCONF当做一个网管协议的话,估计它会在冷板凳上一直坐下去,而如果我们换一个角度去看待NETCONF协议,你会发现也许它是最适合SDN的一个协议。
NETCONF的自动化配置系统采用Client/Server架构,而netopeer即实现了netconf的C/S框架的开源项目。
Netopeer是基于开源项目libnetconf库完成的,已实现client和server端的代码。主要涉及的组件为netopeer-cli和netopeer-server;其中netopeer-cli为一个CLI程序,允许通过该程序连接到netconfserver,和操纵它的配置数据;netopeer-server为一个netconf服务器端的守护进程,允许与netconf client建立连接,接收配置数据等操作。除了这两个重要的模块,netopeer项目还包含了libnetconf transAPI模块举例,位于项目源码transAPI/路径下,例如cfgsystem模块,实现的是一个ietf-system数据模型。
NETCONF是一个基于XML的交换机配置接口,用于替代CLI、SNMP等配置交换机。
本质上来说,NETCONF就是利用XML-RPC的通讯机制实现配置客户端和配置服务端之间的通信,实现对网络设备的配置和管理。
NETCONF分为四个层:安全传输层、消息层、操作层、内容层。
安全传输层:用于跟交换机安全通信,NETCONF并未规定具体使用哪种传输层协议,所以可以使用SSH、TLS、HTTP等各种协议
消息层:提供一种传输无关的消息封装格式,用于RPC通信
操作层:定义了一系列的RPC调用方法,并可以通过Capabilities来扩展
内容层:定义RPC调用的数据内容
NETCONF关键技术实现:
关键的环节包括:安全认证、建立加密传输通道、rpc-xml消息收发、rpc-xml文件解析、rpc-reply消息的生成。
基于centos7搭建环境:
1 | sudo yum install libtool |
1 | git clone https://github.com/mbj4668/pyang.git |
1 | sudo yum install libssh-devel |
1 | git clone https://github.com/CESNET/libnetconf.git |
1 | git clone https://github.com/CESNET/netopeer.git |
启动netopeer server
1 | netopeer-server -d |
验证 netopeer-cli 与 netopeer-server 连接
1 | netopeer-cli |
其它操作
1 | a) 配置netopeer server模块 |
假设经过第一步后成功编译出libnetconf和netopeer,这样我们就可以直接运行netopeer。netconf默认监听端口是830端口。
众所周知,netconf协议支持自定义rpc,因此此步骤需要做的是如何在现有netconf中增加自己的yang模型以及执行自己的rpc??
这里就需要用到这个工 lnctool
。这个工具是用python实现的,里面代码也比较简单,比如说调用其他应用程序(pyang)或者直接写文件。
1 | lnctool --model ./turing-machine.yang transapi --paths ./path |
另外一个重点是就是实现源文件中相关接口—rpc函数。当经过以上两个步骤之后,就可以进行编译,默认编译出动态库.so文件。
当我们把rpc函数实现之后,就可以通过另外一个工具,netopeer-manager安装自定义模型,使用命令行如下:
1 | netopeer-manager add --name [module name] --model [model path] --transapi [model share library] --datastore [module datastore file] |
1 | autoreconf --force --install |
1 | cat /usr/local/etc/netopeer/cfgnetopeer/datastore.xml |
1 | su root |
1 | netopeer-manager list |
1 | netopeer-server -v 3 |
使用命令:
1 | netopeer-cli |
1 | cat /usr/local/etc/netopeer/turing-machine/datastore.xml |
OK, unlock candidate:
1 | unlock candidate |
unlock is successfully
server’s output:
1 | netopeer-server[4981]: 4981-D 1-26 14:38: 4 11--Received message (session 1): <?xml version="1.0" encoding="UTF-8"?> |
running datestore 内容:
1 | netconf> get-config running |
准备get xml文件:
1 | <!-- turing-machine-get-test.xml --> |
netopeer-cli 命令:
1 | netconf> get --filter=/turing-machine-get-test.xml |
netopeer-server log:
1 | netopeer-server[127219]: Received message (session 4): |
要查找要添加YANG模块的默认位置,请运行
1 | sudo /usr/bin/netopeer-configurator |
你可以在[Netopeer]一栏找到以下信息
1 | [Netopeer] Using modules installed in path: /usr/local/etc/netopeer/modules.conf.d |
默认情况下,所有模块的XML实例数据(Netopeer调用这个数据存储,不幸的是,使用与NETCONF的运行/候选/启动数据存储相同的术语)存储在 /usr/local/etc/Netopeer/modules.conf.d
中
当您使用 netopier -manager
添加模块时,—datastore
选项应该指向 /usr/local/etc/netopeer/modules.conf.d
使用 netopier -manager
的例子:
首先检查哪些模块是默认启用的:
1 | netopeer-manager list |
没有安装模块。让我们添加一些模块。要添加模型,首先使用PyangNETCONFc转换 .yang
文件 yin文件
。Netopeer内部使用 YIN 格式。例如,让我们添加 toaster 数据存储,这样您就可以使用NETCONFc来配置它。
Download toaster.yang from http://seguesoft.com/get-standard-yang-modules. Then you can do:
1 | pyang -f yin /home/bob/YANG_modules/toaster.yang -o /home/bob/YANG_modules/toaster.yin |
Then add toaster.yin’s datastore into Netopeer as follows
1 | netopeer-manager add --name toaster --model /home/bob/YANG_modules/toaster.yin --datastore /usr/local/etc/netopeer/modules.conf.d/toaster.xml |
对于命令引用类型
1 | netopeer-manager add --help |
1 | xml 生成 |
启动服务测试:
1 | sudo /usr/local/bin/netopeer-server -v 3 |
安装好 turing-machine 之后,可以对模块做一些操作,以下为实际的操作过程以及对应的xml编写示例。
<edit-config>
详解:
描述:
``操作将全部或部分指定配置加载到指定的目标配置数据存储。此操作允许以多种方式表示新配置,例如使用本地文件,远程文件或内联。如果目标配置数据存储不存在,它将被创建。
如果一个NETCONF
节点(peer
)支持:url
能力(8.8节),那么元素可以出现而不是
参数。
设备分析源和目标配置并执行所请求的更改。目标配置不一定被替换,就像``消息一样。而是根据源数据和请求的操作来更改目标配置。
如果``操作包含应用于基础数据模型中相同概念节点的多个子操作,则操作的结果是未定义的(即,不在NETCONF协
议的范围之内)。
属性:
operation
:子树中的元素可以包含一个“`operation`”属性,它属于[3.1](https://tools.ietf.org/html/rfc6241#section-3.1)节定义的`NETCONF`名称空间。该属性标识配置中的要执行该操作的点,并可以在整个
子树中的多个元素上出现。如果未指定“operation
”属性,则配置将合并到配置数据存储中。 “operation
”属性具有以下值之一:merge
:由包含此属性的元素标识的配置数据与由``参数标识的配置数据存储中对应级别的配置合并。这是默认行为。replace
:由包含此属性的元素标识的配置数据将替换由参数标识的配置数据存储区中的任何相关配置。如果配置数据存储中不存在此类配置数据,则会创建它。与替换整个目标配置的
操作不同,只有实际存在于``参数中的配置受到影响。create
:当且仅当配置数据存在于配置数据存储中时,才将包含此属性的元素标识的配置数据添加到配置中。如果配置数据存在,则返回一个值为“`data-exists`”的
元素。delete
:当且仅当配置数据当前存在于配置数据存储中时,才从配置中删除由包含此属性的元素标识的配置数据。如果配置数据不存在,则返回一个值为“`data-missing`”的
元素。remove
:如果配置数据当前存在于配置数据存储中,则从配置中删除由包含此属性的元素标识的配置数据。如果配置数据不存在,服务器会自动忽略“remove
”操作。参数:
target
:正在编辑的配置数据存储的名称,例如或
。default-operation
:选择此请求的默认操作(如“`operation`”属性中所述)。
参数的默认值是“merge
”。``参数是可选的,但是如果提供,它具有以下值之一:merge
:``参数中的配置数据与目标数据存储中相应级别的配置合并。这是默认行为。replace
:``参数中的配置数据完全替换了目标数据存储中的配置。这对加载以前保存的配置数据很有用。none
:目标数据存储不受参数中的配置影响,除非和直到传入的配置数据使用“`operation`”属性请求不同的操作。如果
参数中的配置包含目标数据存储中没有相应级别的数据,则返回,并带有
缺少数据的值。使用“none
”允许像“delete
”这样的操作避免无意中创建要删除的元素的父层次结构。test-option
:只有当设备公布支持:validate:1.1
能力时才能指定元素([8.6节](https://tools.ietf.org/html/rfc6241#section-8.6))。
元素具有以下值之一:test-then-set
:在尝试设置之前执行验证测试。 如果发生验证错误,请不要执行``操作。 这是默认的测试选项。set
:先执行一个没有验证测试的设置。test-only
:仅执行验证测试,而不尝试设置。error-option
:``元素具有以下值之一:stop-on-error
:停止第一个错误的``操作。这是默认的错误选项。continue-on-error
:继续处理错误的配置数据;记录错误,如果发生任何错误,则产生否定响应。rollback-on-error
:如果发生错误情况,从而生成错误严重性元素,则服务器将停止处理
操作,并在指定的开始时将指定的配置恢复到完整状态这个``操作。该选项要求服务器支持8.5节中描述的错误回滚功能。config
:配置数据的层次结构,由设备的一个数据模型定义。内容必须放置在适当的命名空间中,以允许设备检测适当的数据模型,并且内容必须遵循该数据模型的约束,如其能力定义所定义。能力在第8节讨论。turing.xml
内容如下:
1 |
|
为 turing-machine 创建实例:
1 | edit-config --defop=merge --config=/root/turing.xml candidate |
delete-delta.xml
内容如下:
1 |
|
删除 delta 中的一项:
1 | edit-config --defop=none --config=/root/delete-delta.xml candidate |
create-delta.xml
内容如下:
1 |
|
创建 delta 数据:
1 | edit-config --defop=none --config=/root/create-delta.xml candidate |
merge-delta.xml
内容如下:
1 |
|
replace-delta.xml
内容如下:
1 |
|
合并和替换 delta:
1 | edit-config --defop=none --config=/root/merge-delta.xml candidate |
get-turing.xml
内容如下:
1 |
|
get-delta-0.xml
内容如下:
1 |
|
获取特定数据:
1 | get --filter=/root/get-turing.xml |
rpc-initialize.xml
内容如下:
1 |
|
执行 rpc-initialize :
1 | user-rpc --file=/root/rpc-initialize.xml |
server 响应内容:
1 | netopeer-server[14444]: Received message (session 7): <?xml version="1.0" encoding="UTF-8"?> |
rpc-run.xml
内容如下:
1 |
|
执行 rpc-run :
1 | user-rpc --file=/root/rpc-run.xml |
server 响应内容:
1 | netopeer-server[14444]: Received message (session 7): <?xml version="1.0" encoding="UTF-8"?> |
TODO
Netconf2
TODO
YANG(RFC 7950)是NETCONF(RFC 6241)的数据建模语言,由IETF NETMOD WG开发。
pyang是一个YANG验证器,转换器和代码生成器,用python编写。 它可用于验证YANG模块的正确性,将YANG模块转换为其他格式,以及从模块生成代码。
sdn、nfv盛行的今天,yang建模语言变得越来越重要,它定义于netconf协议,但是却超越了netconf协议本身,在网络世界迸发自己的活力。
如今最大的开源sdn控制器-opendaylight以yang作为建模语言进行核心模型存储,netconf以及restconf纷纷依靠yang模型定义接口,定义南向模型、北向模型。最具sdn气息的openflow也有个伴侣协议of-config使用yang建模,借助netconf通道下发相关配置。
yang很重要,但是用好yang可以选择的工具却并不多,pyang就是其中很重要的一个,这是一个由python代码编写的yang语法验证器、转换器以及代码生成器,一些开源软件使用它构建模型校验语法,比如开源netconf agent netopeer,我们作为用户也可以使用它进行语法校验,生成tree、yin等其它格式模型、数据。它是一个命令行,是一个学习好yang之路的一个好用的工具。
pyang命令行的使用,提供了丰富的文档说明通过pyang –help或者man pyang都能看到非常详细的信息:
普通用户模式转换主要关注如下几个即可:
pyang的格式 yin和yang转换很简单,按照下面命令完成即可:
1 | pyang -f yin -o ietf-yang-types.yin ietf-yang-types.yang |
tree 文件是 yang 独有的一个文件,主要功能就是为 yang 生成一个快速化的浏览视图.
如下所示为一个批量处理 yang 文件生成 tree 文件的命令行:
1 | pyang -f tree yangdir/*.yang -o ouputdir/output.tree |
因为没有仔细理解功能,这里只是将功能简单尝试了以下,后续有机会再系统的梳理一下这个功能,此处只给出简单的说明:
1 | pyang --ietf test.yang |
总的来说pyang是我们使用yang的一个必不可少的命令行、工具和插件,这里给出的一只是笔者平时的一些使用经验,它还有许多强大的功能在这里没有一一详述。
yang-explorer, 一个开源的杨浏览器和 RPC Builder
An open-source Yang Browser and RPC Builder Application
源代码名称:yang-explorer
源代码网址:http://www.github.com/CiscoDevNet/yang-explorer
yang-explorer源代码文档
yang-explorer源代码下载
Git URL:
1 | git://www.github.com/CiscoDevNet/yang-explorer.git |
Git Clone代码到本地:
1 | git clone http://www.github.com/CiscoDevNet/yang-explorer |
Subversion代码到本地:
1 | svn co --depth empty http://www.github.com/CiscoDevNet/yang-explorer |
功能
1 | If already installed, make sure that pip/setuptools are upto date (commands may vary) |
1 | Ubuntu: sudo apt-get install python-virtualenv |
1 | Ubuntu: sudo apt-get install graphviz |
Download 和 安装:
1 | git clone https://github.com/CiscoDevNet/yang-explorer.git |
有关更多信息,请参见第 7节疑难解答:
1 | If you get installation error for missing python.h or xmlversion.h try installing |
更新 exising 安装
1 | cd<install-root>/yang-explorer |
备份数据
可以从数据目录备份YangExplorer数据,并且可以移植到新服务器上。
1 | cp -r <install-root>/yang-explorer/server/data <backup-location>/data |
从备份位置还原
1 | cd<install-root>/yang-explorer/server |
运行 YangExplorer, localhost Start 服务器:
1 | cd<install-root>/yang-explorer |
Start 资源管理器:
1 | http://localhost:8088/static/YangExplorer.html |
运行 ip 地址( 共享服务器) Start 服务器
1 | Determine <ip-address> using if-config# Add ip-address/port in YangExplorer.html after following line:cd<install-root>/yang-explorer/server/static |
Start 资源管理器:
1 | http://<ip-address>:8088/static/YangExplorer.html |
创建 virtual python env
python环境神器virtualenvwrapper安装与使用
NETCONF协议netopeer软件安装与环境搭建
Set up Netopeer Server to use with NETCONFc
NETCONF协议之netopeer软件安装
软件定义网络基础—NETCONF协议
Netconf配置及其RPC和Notification下发流程解析
NETCONF协议详解
NETCONF模块设计介绍
netopeer工具的使用
【开源推介02-pyang】-你离yang模型只差一个pyang工具
An error occurred after executing the ‘commit‘’ command
cannot execute lock/unlock from netopeer-cli
YANG Tools:Yang1.1 Draft Yang Tools: Yang1.1 Draft
edit-config with delete=”operation” not working
]]>关于RFC6241中文翻译
YANG 1.1 数据建模语言
网络基础
NETCONF Support for Event Notifications
Custom Subscription to Event Streams
YANG模型介绍及语法
SDN实战团分享(七):YANG模型与OpenDaylight南北向接口
深入浅出理解 YANG 模型
配置NETCONF/YANG并且验证Cisco IOS XE 16.x平台的示例
Yang解析
CloudEngine 7800&6800&5800 V100R003C00 配置指南-网络管理与监控配置
NE20E-S2 V800R010C10SPC500 配置指南 - 系统管理 01
NETCONF 学习 – python
安装 vimplus:
1 | git clone https://github.com/chxuan/vimplus.git ~/.vimplus |
更新 vimplus:
1 | ./update.sh |
可通过 vimplus 的 ,h
命令查看 vimplus 帮助文档
Ubuntu vimplus .vimrc
文件中有一个插件有问题,需要注释掉,插件名字如下:
1 | Plug 'Shougo/echodoc.vim' |
重新安装 ctags,使用 Universal CTags (默认的软件源都是Exuberant Ctags,版本太旧了)
1 | sudo apt install autoconf |
安装完毕需要在 .vimrc
中添加:
1 | " 正确设置 vimrc,读取 tags(当前目录,否则向上级目录查找添加 tags) |
这时已经可以通过在项目根目录运行 ctags -R .
来生成 tags 文件,就可以用了。
请首先安装最新版本 gtags,目前版本是 6.6.2,Linux 下请自行编译最新版(Debian / Ubuntu 自带的都太老了),Mac 下检查下 brew 安装的版本至少不要低于 6.6.0 ,否则请自己编译。
安装 gtags (系统软件源一般版本比较低,建议自己编译安装)
gtags 原生支持 6 种语言(C,C++,Java,PHP4,Yacc,汇编), 通过安装 pygments
扩展支持 50+ 种语言(包括 go/rust/scala 等,基本覆盖所有主流语言)。
1 | pip install pygments |
保证 .vimrc
里要设置过两个环境变量才能正常工作:
1 | " vimrc 中设置环境变量启用 pygments |
第一个 GTAGSLABEL 告诉 gtags 默认 C/C++/Java 等六种原生支持的代码直接使用 gtags 本地分析器,而其他语言使用 pygments 模块。
第二个环境变量必须设置,否则会找不到 native-pygments 和 language map 的定义,Linux 下要到 /usr/local/share/gtags 里找,也可以把它拷贝成 ~/.globalrc ,Vim 配置的时候方便点。
实际使用 pygments 时,gtags 会启动 python 运行名为 pygments_parser.py 的脚本,通过管道和它通信,完成源代码分析,故需保证 gtags 能在 $PATH 里调用 python,且这个 python 安装了 pygments 模块。
正确安装后,可以通过命令行 gtags 命令和 global 进行测试,注意shell 下设置环境变量。
安装三个插件 : vim-gutentags 索引自动管理 + 索引数据库切换 + 索引预览
1 | " 静态语法检查插件 |
使用 vim-gutentags 插件。
1 | Plug 'ludovicchabant/vim-gutentags' |
.vimrc
里加入:
1 | " gutentags 搜索工程目录的标志,当前文件路径向上递归直到碰到这些文件/目录名 |
在为当前目录生成tags文件后,可以通过按键 Ctrl + ]
跳转到对应的定义位置,再使用命令 Ctrl + o
回退到原来的位置。关于跳转的具体应用,可以参考 Vim使用ctags实现函数跳转
另外,建议多使用 Ctrl + W + ]
用新窗口打开并查看光标下符号的定义,或者 Ctrl -W }
使用 preview 窗口预览光标下符号的定义。
预设快捷键如下
快捷键 | 说明 |
---|---|
<leader>cg | 查看光标下符号的定义 |
<leader>cs | 查看光标下符号的引用 |
<leader>cc | 查看有哪些函数调用了该函数 |
<leader>cf | 查找光标下的文件 |
<leader>ci | 查找哪些文件 include 了本文件 |
查找到索引后跳到弹出的 quikfix 窗口,停留在想查看索引行上,按 小P
直接打开预览窗口,大P
关闭预览。
我们从新项目仓库里查询了一个符号的引用,gtags 噼里啪啦的给了你二十多个结果,那么多结果顺着一个个打开,查看,关闭,再打开很蛋疼,可使用 vim-preview 插件高效的在 quickfix 中先快速预览所有结果,再有针对性的打开必要文件:
1 | Plug 'skywind3000/vim-preview' |
以下是部分常用快捷键,可通过 vimplus 的 ,h
命令查看 vimplus帮助文档。
快捷键 | 说明 |
---|---|
<leader>n | 打开/关闭代码资源管理器 |
<leader>t | 打开/关闭函数列表 |
<leader>a | .h .cpp 文件切换 |
<leader>u | 转到函数声明 |
<leader>U | 转到函数实现 |
<leader>u | 转到变量声明 |
<leader>o | 打开include文件 |
<leader>y | 拷贝函数声明 |
<leader>p | 生成函数实现 |
<leader>w | 单词跳转 |
<leader>f | 搜索~目录下的文件 |
<leader>F | 搜索当前目录下的文本 |
<leader>ff | 语法错误自动修复(FixIt) |
<c-p> | 切换到上一个buffer |
<c-n> | 切换到下一个buffer |
<leader>d | 删除当前buffer |
<leader>D | 删除当前buffer外的所有buffer |
<F5> | 显示语法错误提示窗口 |
<leader>l | 按竖线对齐 |
<leader>= | 按等号对齐 |
Ya | 复制行文本到字母a |
Da | 剪切行文本到字母a |
Ca | 改写行文本到字母a |
rr | 替换文本 |
<leader>r | 全局替换,目前只支持单个文件 |
rev | 翻转当前光标下的单词或使用V模式选择的文本 |
gcc | 注释代码 |
gcap | 注释段落 |
vif | 选中函数内容 |
dif | 删除函数内容 |
cif | 改写函数内容 |
vaf | 选中函数内容(包括函数名 花括号) |
daf | 删除函数内容(包括函数名 花括号) |
caf | 改写函数内容(包括函数名 花括号) |
fa | 查找字母a,然后再按f键查找下一个 |
<leader>h | 打开vimplus帮助文档 |
<leader>H | 打开当前光标所在单词的vim帮助文档 |
<leader><leader>t | 生成try-catch代码块 |
<leader><leader>y | 复制当前选中到系统剪切板 |
<leader><leader>i | 安装插件 |
<leader><leader>u | 更新插件 |
<leader><leader>c | 删除插件 |
快捷键 | 说明 |
---|---|
:e <filename> | 新建buffer打开文件 |
:bp | 切换到上一个buffer |
:bn | 切换到下一个buffer |
:bd | 删除当前buffer |
快捷键 | 说明 |
---|---|
:sp <filename> | 横向切分窗口并打开文件 |
:vsp <filename> | 竖向切分窗口并打开文件 |
<c-w>h | 跳到左边的窗口 |
<c-w>j | 跳到下边的窗口 |
<c-w>k | 跳到上边的窗口 |
<c-w>l | 跳到右边的窗口 |
<c-w>c | 关闭当前窗口 |
<c-w>o | 关闭其他窗口 |
:only | 关闭其他窗口 |
快捷键 | 说明 |
---|---|
0 | 光标移动到行首 |
^ | 跳到从行首开始第一个非空白字符 |
$ | 光标移动到行尾 |
<c-o> | 跳到上一个位置 |
<c-i> | 跳到下一个位置 |
<c-b> | 上一页 |
<c-f> | 下一页 |
<c-u> | 上移半屏 |
<c-d> | 下移半屏 |
H | 调到屏幕顶上 |
M | 调到屏幕中间 |
L | 调到屏幕下方 |
:n | 跳到第n行 |
w | 跳到下一个单词开头(标点或空格分隔的单词) |
W | 跳到下一个单词开头(空格分隔的单词) |
e | 跳到下一个单词尾部(标点或空格分隔的单词) |
E | 跳到下一个单词尾部(空格分隔的单词) |
b | 上一个单词头(标点或空格分隔的单词) |
B | 上一个单词头(空格分隔的单词) |
ge | 上一个单词尾 |
% | 在配对符间移动, 可用于()、{}、[] |
gg | 到文件首 |
G | 到文件尾 |
fx | 跳转到下一个为x的字符 |
Fx | 跳转到上一个为x的字符 |
tx | 跳转到下一个为x的字符前 |
Tx | 跳转到上一个为x的字符前 |
; | 跳到下一个搜索的结果 |
[[ | 跳转到函数开头 |
]] | 跳转到函数结尾 |
快捷键 | 说明 |
---|---|
r | 替换当前字符 |
R | 进入替换模式,直至 ESC 离开 |
s | 替换字符(删除光标处字符,并进入插入模式,前可接数量) |
S | 替换行(删除当前行,并进入插入模式,前可接数量) |
cc | 改写当前行(删除当前行并进入插入模式),同 S |
cw | 改写光标开始处的当前单词 |
ciw | 改写光标所处的单词 |
caw | 改写光标所处的单词,并且包括前后空格(如果有的话) |
ct, | 改写到逗号 |
c0 | 改写到行首 |
c^ | 改写到行首(第一个非零字符) |
c$ | 改写到行末 |
C | 改写到行末(同 c$) |
ci" | 改写双引号中的内容 |
ci' | 改写单引号中的内容 |
ci) | 改写小括号中的内容 |
ci] | 改写中括号中内容 |
ci} | 改写大括号中内容 |
cit | 改写 xml tag 中的内容 |
cis | 改写当前句子 |
ciB | 改写’{}’中的内容 |
c2w | 改写下两个单词 |
ct( | 改写到小括号前 |
x | 删除当前字符,前面可以接数字,3x代表删除三个字符 |
X | 向前删除字符 |
dd | 删除当前行 |
d0 | 删除到行首 |
d^ | 删除到行首(第一个非零字符) |
d$ | 删除到行末 |
D | 删除到行末(同 d$) |
dw | 删除当前单词 |
dt, | 删除到逗号 |
diw | 删除光标所处的单词 |
daw | 删除光标所处的单词,并包含前后空格(如果有的话) |
di" | 删除双引号中的内容 |
di' | 删除单引号中的内容 |
di) | 删除小括号中的内容 |
di] | 删除中括号中内容 |
di} | 删除大括号中内容 |
diB | 删除’{}’中的内容 |
dit | 删除 xml tag 中的内容 |
dis | 删除当前句子 |
d2w | 删除下两个单词 |
dt( | 删除到小括号前 |
dgg | 删除到文件头部 |
dG | 删除到文件尾部 |
d} | 删除下一段 |
d{ | 删除上一段 |
u | 撤销 |
U | 撤销整行操作 |
CTRL-R | 撤销上一次 u 命令 |
J | 连接若干行 |
gJ | 连接若干行,删除空白字符 |
. | 重复上一次操作 |
~ | 交换大小写 |
g~iw | 替换当前单词的大小写 |
gUiw | 将单词转成大写 |
guiw | 将当前单词转成小写 |
guu | 全行转为小写 |
gUU | 全行转为大写 |
gg=G | 缩进整个文件 |
=a{ | 缩进光标所在代码块 |
=i{ | 缩进光标所在代码块,不缩进”{“ |
<< | 减少缩进 |
>> | 增加缩进 |
== | 自动缩进 |
CTRL-A | 增加数字 |
CTRL-X | 减少数字 |
p | 粘贴到光标后 |
P | 粘贴到光标前 |
v | 开始标记 |
y | 复制标记内容 |
V | 开始按行标记 |
CTRL-V | 开始列标记 |
y$ | 复制当前位置到本行结束的内容 |
yy | 复制当前行 |
Y | 复制当前行,同 yy |
yt, | 复制到逗号 |
yiw | 复制当前单词 |
"+y | 复制当前选中到系统剪切板 |
3yy | 复制光标下三行内容 |
v0 | 选中当前位置到行首 |
v$ | 选中当前位置到行末 |
vt, | 选中到逗号 |
viw | 选中当前单词 |
vi) | 选中小括号内的东西 |
vi] | 选中中括号内的东西 |
viB | 选中’{}’中的内容 |
vis | 选中句子中的东西 |
gv | 重新选择上一次选中的文字 |
:set paste | 允许粘贴模式(避免粘贴时自动缩进影响格式) |
:set nopaste | 禁止粘贴模式 |
"?yy | 复制当前行到寄存器 ? ,问号代表 0-9 的寄存器名称 |
"?p | 将寄存器 ? 的内容粘贴到光标后 |
"?P | 将寄存器 ? 的内容粘贴到光标前 |
:registers | 显示所有寄存器内容 |
:[range]y | 复制范围,比如 :20,30y 是复制20到30行,:10y 是复制第十行 |
:[range]d | 删除范围,比如 :20,30d 是删除20到30行,:10d 是删除第十行 |
ddp | 交换两行内容:先删除当前行复制到寄存器,并粘贴 |
快捷键 | 说明 |
---|---|
:w <filename> | 按名称保存文件 |
ZZ | 保存文件(如果有改动的话),并关闭窗口 |
:e <filename> | 打开文件并编辑 |
:saveas <filename> | 另存为文件 |
:r <filename> | 读取文件并将内容插入到光标后 |
:r !dir | 将dir命令的输出捕获并插入到光标后 |
:wa | 保存所有文件 |
:cd <path> | 切换Vim当前路径 |
:new | 打开一个新的窗口编辑新文件 |
:enew | 在当前窗口创建新文件 |
:vnew | 在左右切分的新窗口中编辑新文件 |
:tabnew | 在新的标签页中编辑新文件 |
快捷键 | 说明 |
---|---|
/pattern | 从光标处向文件尾搜索 pattern |
?pattern | 从光标处向文件头搜索 pattern |
n | 向同一方向执行上一次搜索 |
N | 向相反方向执行上一次搜索 |
* | 向前搜索光标下的单词 |
# | 向后搜索光标下的单词 |
:s/p1/p2/g | 替换当前行的p1为p2 |
:%s/p1/p2/g | 替换当前文件中的p1为p2 |
:%s/<p1>/p2/g | 替换当前文件中的p1单词为p2 |
:%s/p1/p2/gc | 替换当前文件中的p1为p2,并且每处询问你是否替换 |
:10,20s/p1/p2/g | 将第10到20行中所有p1替换为p2 |
:%s/1\\2\/3/123/g | 将“1\2/3” 替换为 “123”(特殊字符使用反斜杠标注) |
:%s/\r//g | 删除 DOS 换行符 ^M |
:g/^\s*$/d | 删除空行 |
:g/test/d | 删除所有包含 test 的行 |
:v/test/d | 删除所有不包含 test 的行 |
:%s/^/test/ | 在行首加入特定字符(也可以用宏录制来添加) |
:%s/$/test/ | 在行尾加入特定字符(也可以用宏录制来添加) |
:sort | 排序 |
:g/^\(.\+\)$\n\1/d | 去除重复行(先排序) |
:%s/^.\{10\}// | 删除每行前10个字符 |
:%s/.\{10\}$// | 删除每行尾10个字符 |
快捷键 | 说明 |
---|---|
vim -u NONE -N | 开启vim时不加载vimrc文件 |
vimdiff file1 file2 | 显示文件差异 |
vim -R filename | 以只读方式打开(阅读模式) |
2018 更新下vim 插件
Vim 8 中 C/C++ 符号索引:GTags 篇
Vim自动生成tags插件vim-gutentags安装和自动跳转方法-Vim插件(10)
在Vim中使用gtags
ubuntu14.04编译gnu global 6.6.3
]]>vimplus – Github
Vim使用笔记
I haven’t met a developer who looked at a conflict message and did not pull their hair strands with frustration.
Trying to resolve each merge conflict is one of those things that every developer hates. Especially if it hits you when you’re gearing up for a production deploy!
This is where having the right Git workflow set up can do a world of good for your development workflow.
Of course, having the right git workflow will not solve all your problems. But it’s a step in the right direction. After all, with every team working remotely, the need to build features together without having your codebase getting disrupted is critical.
How you set it up depends on the project you’re working on, the release schedules your team has, the size of the team, and more!
In this article, we’ll walk you through 5 different git workflows, their benefits, their cons, and when you should use them. Let’s jump in!
The most basic git workflow is the one where there is only one branch — the master branch. Developers commit directly into it and use it to deploy to the staging and production environment.
This workflow isn’t usually recommended unless you’re working on a side project and you’re looking to get started quickly.
Since there is only one branch, there really is no process over here. This makes it effortless to get started with Git. However, some cons you need to keep in mind when using this workflow are:
The Git Feature Branch workflow becomes a must have when you have more than one developer working on the same codebase.
Imagine you have one developer who is working on a new feature. And another developer working on a second feature. Now, if both the developers work from the same branch and add commits to them, it would make the codebase a huge mess with plenty of conflicts.
To avoid this, the two developers can create two separate branches from the master branch and work on their features individually. When they’re done with their feature, they can then merge their respective branch to the master branch, and deploy without having to wait for the second feature to be completed.
The Pros of using this workflow is, the git feature branch workflow allows you to collaborate on code without having to worry about code conflicts.
This workflow is one of the more popular workflows among developer teams. It’s similar to the Git Feature Branch workflow with a develop branch that is added in parallel to the master branch.
In this workflow, the master branch always reflects a production-ready state. Whenever the team wants to deploy to production they deploy it from the master branch.
The develop branch reflects the state with the latest delivered development changes for the next release. Developers create branches from the develop branch and work on new features. Once the feature is ready, it is tested, merged with develop branch, tested with the develop branch’s code in case there was a prior merge, and then merged with master.
The advantage of this workflow is, it allows teams to consistently merge new features, test them in staging, and deploy to production. While maintaining code is easier, it can get a little tiresome for some teams since it can feel like going through a tedious process.
The gitflow workflow is very similar to the previous workflow we discussed combined with two other branches — the release branch and the hot-fix branch.
The hot-fix branch
The hot-fix branch is the only branch that is created from the master branch and directly merged to the master branch instead of the develop branch. It is used only when you have to quickly patch a production issue. An advantage of this branch is, it allows you to quickly deploy a production issue without disrupting others’ workflow or without having to wait for the next release cycle.
Once the fix is merged into the master branch and deployed, it should be merged into both develop and the current release branch. This is done to ensure that anyone who forks off develop to create a new feature branch has the latest code.
The release branch
The release branch is forked off of develop branch after the develop branch has all the features planned for the release merged into it successfully.
No code related to new features is added into the release branch. Only code that relates the release is added to the release branch. For example, documentation, bug fixes, and other tasks related to this release are added to this branch.
Once this branch is merged with master and deployed to production, it’s also merged back into the develop branch, so that when a new feature is forked off of develop, it has the latest code.
This workflow was first published and made popular by Vincent Driessen and since then it has been widely used by organizations that have a scheduled release cycle.
Since the git-flow is a wrapper around Git, you can install git-flow in your current repository. It’s a straightforward process and it doesn’t change anything in your repository other than creating branches for you.
To install on a Mac machine, execute brew install git-flow
in your terminal.
To install on a Windows machine, you’ll need to download and install the git-flow. After the installation is done, run git flow init
to use it in your project.
The Fork workflow is popular among teams who use open-source software.
The flow usually looks like this:
Git和其他版本控制系统(包括SVN和近似工具)的主要差别在于Git对待数据的方法。概念上来区分,其他大部分系统以文件变更列表的方式存储信息。这类系统(CVS、Subversion、Perforce、Bazaar等等)将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。Git不按照以上方式对待或保存数据。反之,Git更像是把数据看作是对小型文件系统的一组快照。每次你提交更新,或在Git中保存项目状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,Git不再重新存储该文件,而是只保留一个链接指向之前存储的文件。Git对待数据更像是一个快照流。如下图所示。这是Git与几乎所有其它版本控制系统的重要区别。
在Git中的绝大多数操作只需要访问本地文件和资源,一般不需要来自网络上其它计算机的信息。如果你习惯于所有操作都有网络延时开销的集中式版本控制系统,Git在这方面会让你感到速度之神赐给了Git超凡的能量。因为你在本地磁盘上就有项目的完整历史,所以大部分操作看起来瞬间完成。
举个例子,要浏览项目的历史,Git不需要外连到服务器去获取历史,然后再显示出来,它只需要直接从本地数据库中读取。你能立即看到项目历史。如果想查看当前版本与一个月前的版本之间引入的修改,Git会查找到一个月前的文件做一次本地的差异计算,而不是由远程服务器处理或从远程服务器拉回旧版本文件再来本地处理。
Git中所有数据在存储前都计算校验和,然后以校验和来引用。这意味着不可能在Git不知情时更改任何文件内容或目录内容。这个功能构建在Git底层,是构成Git哲学不可或缺的部分。若你在传送过程中丢失信息或损坏文件,Git就能发现。
你执行的Git操作,几乎只往Git数据库中增加数据。很难让Git执行任何不可逆的操作,或者让它以任何形式清除数据。同别的VCS一样,未提交更新时有可能丢失或弄乱修改的内容,但是一旦你提交快照到Git中,就难以再丢失数据,特别是如果你定期的推送数据库到其它仓库的话。
Workspace(工作区):
我们平时进行开发改动的地方,是我们当前看到的,也是最新的。平常我们开发就是拷贝远程仓库中的一个分支,基于该分支进行开发,在开发过程中就是对工作区的操作。
Index / Stage(暂存区):
.git
目录下的 index 文件,暂存区会记录 git add
添加文件的相关信息(文件名、大小、timestamp…),不保存文件实体,通过 id 指向每个文件实体。可以使用 git status
查看暂存区的状态。暂存区标记了你当前工作区中,哪些内容是被 git 管理的。
当我们完成某个需求或功能后需要提交到远程仓库,那么第一步就是通过 git add
命令先提交到暂存区,被 git 管理。
Local Repository(本地仓库):
保存了对象被提交过的各个版本,比起工作区和暂存区的内容,它要更旧一些。git commit
后同步index(暂存区)的目录树到本地仓库,方便从下一步通过 git push
同步本地仓库与远程仓库。
Remote Repository(远程仓库):
远程仓库是指托管在一些Git代码托管平台上的你的项目的版本库,比如GitHub、GitLab、码云、码市等等。远程仓库的内容可能被分布在多个地点的处于协作关系的本地仓库修改,因此它可能与本地仓库同步,也可能不同步,但是远程仓库的内容是最旧的。
四个区域之间的关系如下图所示:
HEAD,它始终指向当前所处分支的最新的提交点,所处的分支发生了变化,或者产生了新的提交点,HEAD就会跟着改变。
git add
主要实现将工作区修改的内容提交到暂存区,交由git管理。
1 | 添加当前目录的所有文件到暂存区 |
git commit
主要实现将暂存区的内容提交到本地仓库,并使得当前分支的HEAD向后移动一个提交点。相关命令如下表所示:
1 | 提交暂存区到本地仓库,message代表说明信息 |
在我们的代码仓库中,有一条主分支Master,我们可以从主分支当中,创建出许多分支以开发其他功能,创建子分支的好处是每个分支互不影响,大家只需要在自己的分支上继续开发,正常工作。待开发完毕后,再将自己的子分支合并到主分支或者其他分支即可,这样,即安全又不影响他人的工作。
关于分支,主要有展示分支、切换分支、创建分支、删除分支操作。相关命令如下表所示:
1 | 列出本地所有分支 |
git merge
命令的作用是把不同的分支合并起来。在实际的开发中,我们会从master主分支中创建出一个新的分支,然后进行需求或功能的开发,最后开发完成后需要合并子分支到主分支master中,这就需要用到git merge
命令。
1 | 合并指定分支到当前分支 |
rebase又称为衍合,是合并的另外一种选择。在开始阶段,我们在新的分支上,执行 git rebase dev
,那么新分支上的commit都在master分支上重演一遍,最后checkout切换回到新的分支。这一点与merge命令是一样的,合并前后所处的分支并没有改变。
git reset
命令把当前分支指向另一个位置,并且有选择的变动工作目录和索引。也用来从历史仓库中复制文件到索引,而不动工作目录。如果不给选项,那么当前分支指向到那个提交。如果用 –-hard
参数,那么工作目录也更新,如果用 –-soft
参数,那么都不变。使用 git reset HEAD ~3
命令的说明如下图所示:
如果没有给出提交的版本号,那么默认用HEAD。这样,分支指向不变,但是索引会回滚到最后一次提交,如果用 –-hard
参数,工作目录也一样。
1 | 只改变提交点,暂存区和工作目录的内容都不改变 |
revert 是用一个新的提交来消除一个历史提交所做的任何修改,revert之后,我们本地的代码会回滚到指定的历史版本。举个例子,其结果如下图所示:
1 | git commit -am 'update readme' |
将本地仓库分支上传到远程仓库分支,实现同步。相关命令如下:
1 | 上传本地指定分支到远程仓库 |
git fetch
是从远程仓库中获取最新版本到本地仓库中,但不会自动合并本地的版本,也就说,我们可以查看更新情况,然后再决定是否进行合并。在fetch命令中,有一个重要的概念:FETCH_HEAD:某个branch在服务器上的最新状态,每一个执行过fetch操作,都会存在一个FETCH_HEAD列表,这个列表保存在 .git
目录的FETCH_HEAD文件中,其中每一行对应于远程服务器的每一个分支。当前分支指向的FETCH_HEAD,就是文件第一行对应的那个分支。
git pull
是从远程仓库中获取最新版本并自动合并到本地的仓库。
1 | git pull origin next |
上面命令表示,取回 origin/next 分支的更新,再与当前分支进行合并。实质上,等同于,先做 git fetch
,再执行 git merge
。
1 | git fetch origin |
举个例子说明:现在我们有这样两个分支,test 和 master,其提交记录如下图所示:
在 master 分支上执行 git merge test
命令后,会得到如下图所示的结果:
如果在master分支上执行 git rebase test
命令,则会得到如下图所示的结果:
由上面的例子可以看出,merge操作会生成一个新的节点,之前的提交分开显示。而rebase操作不会生成新的节点,是将两个分支融合成一个线性的提交记录。
如果我们想要一个干净的,没有merge commit的线性历史树,那么应该选择git rebase
,如果想保留完整的历史记录,并且想要避免重写commit history的风险,应该选择使用git merge
。
git revert
是用一次逆向的commit“中和”之前的提交,因此日后合并老的分支时,导致这部分改变不会再次出现,减少冲突。但是 git reset
是直接把某个 commit 在某个分支上删除,因而和老的分支再次 merge 时,这些被回滚的commit应该还会被引入,产生很多冲突。git reset
是把HEAD向后移动了一下,而 git revert
是HEAD继续前进,只是新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容。git fetch
和 git pull
共同点都是从远程的分支获取最新的版本到本地,但fetch命令不会自动将更新合并到本地的分支,而pull命令会自动合并到本地的分支。在实际的使用中,git fetch
要更安全一些,因为获取到最新的更新后,我们可以查看更新情况,然后再决定是否合并。
上面的四条命令在工作目录、暂存目录(也叫做索引)和仓库之间复制文件。
git add files
把当前文件放入暂存区域。git commit
给暂存区域生成快照并提交。git reset -- files
用来撤销最后一次 git add files
,你也可以用 git reset
撤销所有暂存区域文件。git checkout -- files
把文件从暂存区域复制到工作目录,用来丢弃本地修改。你可以用 git reset -p
, git checkout -p
, git add -p
进入交互模式。
也可以跳过暂存区域直接从仓库取出文件或者直接提交代码。
git commit -a
相当于运行 git add
把所有当前目录下的文件加入暂存区域再运行 git commit
.git commit files
进行一次包含最后一次提交加上工作目录中文件快照的提交。并且文件被添加到暂存区域。git checkout HEAD -- files
回滚到复制最后一次提交。后文中以下面的形式使用图片。
绿色的 5 位字符表示提交的 ID,分别指向父节点。分支用橘色显示,分别指向特定的提交。当前分支由附在其上的 HEAD 标识。 这张图片里显示最后 5 次提交,ed489 是最新提交。 master 分支指向此次提交,另一个maint 分支指向祖父提交节点。
有许多种方法查看两次提交之间的变动。下面是一些示例。
提交时,git
用暂存区域的文件创建一个新的提交,并把此时的节点设为父节点。然后把当前分支指向新的提交节点。下图中,当前分支是 master。 在运行命令之前,master 指向 ed489,提交后,master 指向新的节点 f0cec 并以 ed489 作为父节点。
即便当前分支是某次提交的祖父节点,git
会同样操作。下图中,在 master 分支的祖父节点 maint 分支进行一次提交,生成了 1800b。 这样,maint 分支就不再是 master 分支的祖父节点。此时,合并 (或者 衍合) 是必须的。
如果想更改一次提交,使用 git commit --amend
。git
会使用与当前提交相同的父节点进行一次新提交,旧的提交会被取消。
另一个例子是 分离HEAD提交, 后文讲。
checkout
命令通常用来从仓库中取出文件,或者在分支中切换。
checkout
命令让 git
把文件复制到工作目录和暂存区域。比如 git checkout HEAD~ foo.c
把文件从 foo.c
提交节点 HEAD~
(当前提交节点的父节点)复制到工作目录并且生成索引。注意当前分支没有变化。
如果没有指定文件名,而是一个本地分支,那么将切换到那个分支去。同时把索引和工作目录切换到那个分支对应的状态。
如果既没有指定文件名,也没有指定分支名,而是一个标签、远程分支、SHA-1 值或者是像 master~3 类似的东西,就得到一个匿名分支,称作 detached HEAD。 这样可以很方便的在历史版本之间互相切换。但是,这样的提交是完全不同的,详细的在下面。
HEAD 是分离的时候, 提交可以正常进行, 但是没有更新已命名的分支 。(可以看作是匿名分支。)
如果此时切换到别的分支,那么所作的工作会全部丢失。注意这个命令之后就不存在 2eecb 了。
如果你想保存当前的状态,可以用这个命令创建一个新的分支: git checkout -b name
。
reset
命令把当前分支指向另一个位置,并且有选择的变动工作目录和索引。也用来在从历史仓库中复制文件到索引,而不动工作目录。
如果不给选项,那么当前分支指向到那个提交。如果用 --hard
选项,那么工作目录也更新,如果用 --soft
选项,那么都不变。
如果没有给出提交点的版本号,那么默认用 HEAD。这样,分支指向不变,但是索引会回滚到最后一次提交,如果用 --hard
选项,工作目录也同样。
如果给了文件名(或者 -p
选项), 那么工作效果和带文件名的checkout差不多,除了索引被更新。
merge
命令把不同分支合并起来。合并前,索引必须和当前提交相同。如果另一个分支是当前提交的祖父节点,那么合并命令将什么也不做。 另一种情况是如果当前提交是另一个分支的祖父节点,就导致 fast-forward 合并。指向只是简单的移动,并生成一个新的提交。
否则就是一次真正的合并。默认把当前提交( ed489 如下所示) 和另一个提交( 33104 )以及他们的共同祖父节点( b325c ) 进行一次三方合并。结果是先保存当前目录和索引,然后和父节点 33104 一起做一次新提交。
cherry-pick
命令”复制”一个提交节点并在当前复制做一次完全一样的新提交。
衍合是合并命令的另一种选择。合并把两个父分支合并进行一次提交,提交历史不是线性的。衍合在当前分支上重演另一个分支的历史,提交历史是线性的。 本质上,这是线性化的自动的 cherry-pick
上面的命令都在 topic 分支中进行,而不是 master 分支,在 master 分支上重演,并且把分支指向新的节点。注意旧提交没有被引用,将被回收。
要限制回滚范围,使用 --onto
选项。下面的命令在 master 分支上重演当前分支从 169a6 以来的最近几个提交,即 2c33a。
同样有 git rebase --interactive
让你更方便的完成一些复杂操组,比如丢弃、重排、修改、合并提交。没有图片体现着下,细节看这里: git-rebase(1)
核心:
git origin -d 分支名
删除远程仓库的分支)。优势:
当前 commit
在哪里,HEAD 就在哪里,这是一个永远自动指向当前 commit
的引用,所以你永远可以用 HEAD 来操作当前 commit
,
HEAD 是 Git 中一个独特的引用,它是唯一的。而除了 HEAD 之外,Git 还有一种引用,叫做 branch(分支)。HEAD 除了可以指向 commit
,还可以指向一个 branch,当指向一个 branch 时,HEAD 会通过branch 间接指向当前 commit
,HEAD 移动会带着 branch 一起移动:
branch 包含了从初始 commit
到它的所有路径,而不是一条路径。并且,这些路径之间也是彼此平等的。
像上图这样,master 在合并了 branch1 之后,从初始 commit
到 master 有了两条路径。这时,master 的串就包含了 1 2 3 4 7
和 1 2 5 6 7
这两条路径。而且,这两条路径是平等的,1 2 3 4 7
这条路径并不会因为它是「原生路径」而拥有任何的特别之处
创建branch:
1 | git branch 名称 |
切换branch:
1 | git checkout 名称 # 将 HEAD指向该 branch |
创建 + 切换:
1 | git checkout -b 名称 |
在切换到新的 branch 后,再次 commit
时 HEAD 就会带着新的 branch 移动了:
而这个时候,如果你再切换到 master 去 commit
,就会真正地出现分叉了:
删除branch:git branch -d 名称
注意:
checkout
把 HEAD 指向其他地方。commit
。(不过如果一个 commit
不在任何一个 branch 的「路径」上,或者换句话说,如果没有任何一个 branch 可以回溯到这条 commit
(也许可以称为野生 commit
?),那么在一定时间后,它会被 Git 的回收机制删除掉)-d
换成 -D
可以强制删除所谓引用,其实就是一个个的字符串。这个字符串可以是一个 commit
的 SHA-1 码(例:c08de9a4d8771144cd23986f9f76c4ed729e69b0),也可以是一个 branch(例:ref: refs/heads/feature3)。
Git 中的 HEAD 和每一个 branch 以及其他的引用,都是以文本文件的形式存储在本地仓库 .git
目录中,而 Git 在工作的时候,就是通过这些文本文件的内容来判断这些所谓的「引用」是指向谁的。
commits
一并上传git push
不加参数只能上传到从远程仓库 clone
或者 pull
下来的分支,如需 push
在本地创建的分支则需使用 git push origin 分支名
的命令push
与本地一致,远端仓库 HEAD 永远指向默认分支(master),并随之移动(可以使用 git br -r
查看远程分支的 HEAD 指向)。含义:从目标 commit
和当前 commit
(即 HEAD 所指向的 commit
)分叉的位置起,把目标 commit
的路径上的所有 commit
的内容一并应用到当前 commit
,然后自动生成一个新的 commit
。
当执行 git merge branch1
操作,Git 会把 5 和 6 这两个 commit
的内容一并应用到 4 上,然后生成一个新的提交 7 。
merge
的特殊情况:
merge
冲突:你的两个分支改了相同的内容,Git 不知道应该以哪个为准。如果在 merge
的时候发生了这种情况,Git 就会把问题交给你来决定。具体地,它会告诉你 merge
失败,以及失败的原因;这时候你只需要手动解决掉冲突并重新 add、commit(改动不同文件或同一文件的不同行都不会产生冲突);或者使用 git merge --abort
放弃解决冲突,取消 merge
commit
:merge
是一个空操作:此时 merge
不会有任何反应。
commit
且不存在分支(fast-forward):git 会直接把 HEAD 与其指向的 branch(如果有的话)一起移动到目标 commit
。
有些人不喜欢 merge
,因为在 merge
之后,commit
历史就会出现分叉,这种分叉再汇合的结构会让有些人觉得混乱而难以管理。如果你不希望 commit
历史出现分叉,可以用 rebase
来代替 merge
。
可以看出,通过 rebase
,5 和 6 两条 commits
把基础点从 2 换成了 4 。通过这样的方式,就让本来分叉了的提交历史重新回到了一条线。这种「重新设置基础点」的操作,就是 rebase
的含义。另外,在 rebase
之后,记得切回 master
再 merge
一下,把 master
移到最新的 commit
。
为什么要从 branch1 来
rebase
,然后再切回 master 再merge
一下这么麻烦,而不是直接在 master 上执行rebase
?从图中可以看出,
rebase
后的每个commit
虽然内容和rebase
之前相同,但它们已经是不同的commit
了(每个commit
有唯一标志)。如果直接从 master 执行rebase
的话,就会是下面这样:这就导致 master 上之前的两个最新
commit
(3和4)被剔除了。如果这两个commit
之前已经在远程仓库存在,这就会导致没法push
:所以,为了避免和远程仓库发生冲突,一般不要从 master 向其他 branch 执行
rebase
操作。而如果是 master 以外的 branch 之间的rebase
(比如 branch1 和 branch2 之间),就不必这么多费一步,直接rebase
就好。
需要说明的是, rebase
是站在需要被 rebase
的 commit
上进行操作,这点和 merge
是不同的。
stash
指令可以帮你把工作目录的内容全部放在你本地的一个独立的地方,它不会被提交,也不会被删除,你把东西放起来之后就可以去做你的临时工作了,做完以后再来取走,就可以继续之前手头的事了。
操作步骤:
git stash
可以加上 save
参数后面带备注信息(git stash save '备注信息'
)git stash pop
弹出第一个 stash
(该 stash 从历史 stash 中移除);或者使用 git stash apply
达到相同的效果(该 stash 仍存在 stash list 中),同时可以使用 git stash list
查看 stash 历史记录并在 apply 后面加上指定的 stash 返回到该 stash。注意:没有被 track 的文件会被 git 忽略而不被 stash,如果想一起 stash,加上 -u
参数。
可以查看 git 的引用记录,不指定参数,默认显示 HEAD 的引用记录;如果不小心把分支删掉了,可以使用该命令查看引用记录,然后使用 checkout 切到该记录处重建分支即可。
注意:不再被引用直接或间接指向的 commits 会在一定时间后被 Git 回收,所以使用 reflog 来找回被删除的 branch 的操作一定要及时,不然有可能会由于 commit 被回收而再也找不回来。
log:查看已提交内容
1 | git log -p # 可以查看每个 commit 的改动细节(到改动文件的每一行) |
diff:查看未提交内容
1 | git diff --staged |
再提一个修复了错误的 commit
?可以是可以,不过还有一个更加优雅和简单的解决方法:commit --amend
。
具体做法:
git commit --amend
提交修改,结果如下图:减少了一次无谓的 commit
。
使用 rebase -i
(交互式 rebase
):
所谓「交互式 rebase
」,就是在 rebase
的操作执行之前,你可以指定要 rebase
的 commit
链中的每一个 commit
是否需要进一步修改,那么你就可以利用这个特点,进行一次「原地 rebase
」。
操作过程:
git rebase -i HEAD^^
说明:在 Git 中,有两个「偏移符号」:
^
和~
。
^
的用法:在commit
的后面加一个或多个^
号,可以把commit
往回偏移,偏移的数量是^
的数量。例如:master^ 表示 master 指向的commit
之前的那个commit
; HEAD^^ 表示 HEAD 所指向的commit
往前数两个commit
。
~
的用法:在commit
的后面加上~
号和一个数,可以把commit
往回偏移,偏移的数量是~
号后面的数。例如:HEAD~5 表示 HEAD 指向的commit
往前数 5 个commit
。
上面这行代码表示,把当前 commit
( HEAD 所指向的 commit
) rebase
到 HEAD 之前 2 个的 commit
上:
commit
对应的操作,commit
为正序排列,旧的在上,新的在下,前面黄色的为如何操作该 commit
,默认 pick
(直接应用该 commit
不做任何改变),修改第一个 commit
为 edit
(应用这个 commit
,然后停下来等待继续修正)然后 :wq
退出编辑页面,此时 rebase
停在第二个 commit
的位置,此时可以对内容进行修改:commit --amend
将修改提交git rebase --continue
继续 rebase
过程,把后面的 commit
直接应用上去,这次交互式 rebase
的过程就完美结束了,你的那个倒数第二个写错的 commit
就也被修正了:reset –hard 丢弃最新的提交
1 | git reset --hard HEAD^ |
HEAD^ 表示 HEAD 往回数一个位置的
commit
,上节刚说过,记得吧?
用交互式 rebase 撤销历史提交
操作步骤与修改历史提交类似,第二步把需要撤销的 commit
修改为 drop
,其他步骤不再赘述。
用 rebase –onto 撤销提交
1 | git rebase --onto HEAD^^ HEAD^ branch1 |
上面这行代码的意思是:以倒数第二个 commit
为起点(起点不包含在 rebase
序列里),branch1 为终点,rebase
到倒数第三个 commit
上。
有的时候,代码 push 到了远程仓库,才发现有个 commit 写错了。这种问题的处理分两种情况:
出错内容在自己的分支
假如是某个你自己独立开发的 branch 出错了,不会影响到其他人,那没关系用前面几节讲的方法把写错的 commit 修改或者删除掉,然后再 push 上去就好了。但是此时会push报错,因为远程仓库包含本地没有的 commits(在本地已经被替换或被删除了),此时直接使用 git push origin 分支名 -f
强制 push。
问题内容已合并到master
git revert 指定commit
它的用法很简单,你希望撤销哪个 commit,就把它填在后面。如:git revert HEAD^
上面这行代码就会增加一条新的 commit,它的内容和倒数第二个 commit 是相反的,从而和倒数第二个 commit 相互抵消,达到撤销的效果。在 revert 完成之后,把新的 commit 再 push 上去,这个 commit 的内容就被撤销了。它和前面所介绍的撤销方式相比,最主要的区别是,这次改动只是被「反转」了,并没有在历史中消失掉,你的历史中会存在两条 commit :一个原始 commit ,一个对它的反转 commit。
1 | git reset --hard |
1 | git reset --mixed(或者不加参数) 指定commit |
checkout的本质是签出指定的 commit,不止可以切换 branch 还可以指定 commit 作为参数,把 HEAD 移动到指定的 commit 上;与 reset 的区别在于只移动 HEAD 不改变绑定的 branch;git checkout --detach
可以把 HEAD 和 branch 脱离,直接指向当前 commit。
一般来说,日常使用只要记住下图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。
下面是我整理的常用 Git 命令清单。几个专用名词的译名如下。
1 | 在当前目录新建一个Git代码库 |
Git的设置文件为 .gitconfig
,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。
1 | 显示当前的Git配置 |
1 | 添加指定文件到暂存区 |
1 | 提交暂存区到仓库区 |
1 | 列出所有本地分支 |
1 | 列出所有tag |
1 | 显示有变更的文件 |
1 | 下载远程仓库的所有变动 |
1 | 恢复暂存区的指定文件到工作区 |
1 | 生成一个可供发布的压缩包 |
]]>常用 Git 命令清单
图解Git[强烈推荐]
图解git原理与日常实用指南
自己动手,丰衣足食。
该库收集了诸多优质资源,教你如何构建一些属于自己的东西,内容主要分为增强现实、区块链、机器人、编辑器、命令行工具、神经网络、操作系统等几大类别。
从名字中可以看出,这个仓库主要是为开发者推荐一些免费编程书籍,但除此同时,上面也会推荐一些免费的编程课程、播客、网站等学习资源。
我们都知道,Linux 默认终端配置的是 bash,但是,自从 Oh My Zsh 横空出世后,不少开发者都将 bash 换成 Oh My Zsh 了。
究其原因,主要是因为 Oh My Zsh 上面提供了非常强大的插件系统,不少插件用上之后,能够大幅提升生产力。
当然,最主要的原因,还是因为 Oh My Zsh 的界面太酷炫了,装上之后简直逼格满满。
如果用这款终端的水友比较多,我们会考虑后面在公众号出一期专题文章,专门讲讲上面都有哪些比较好用的插件。
这个项目此前我们也在 GitHubDaily 公众号上分享过,该项目作者 John 为了希望获得进入 Google 工作的机会,投入了大量精力去学习。
学习过程中,他接触到了大量与编程相关的知识与教学资源,秉着前人栽树后人乘凉的精神,John 在 GitHub 上开源了这份学习指南。
最后,虽然 John 没去成 Google,去了 Amazon,但他开源的这份资源让无数开发者受益匪浅。
John 的个人成长与学习经历也颇为精彩,感兴趣的同学,可看我们之前的分享过的这篇文章:
GitHub 标星 8w!学完这份指南后,你就可以去 Google 面试了!
GitHub Star:97,100
https://github.com/github/gitignore
相信大部分初用 Git 的工程师,都有着一个苦恼,每次都得针对不同项目、不同语言类型来重复写 .gitignore
,以忽略一些无需纳入 Git 管理的文件。
这个项目诞生的意义,就是帮工程师解决这个问题的。每次你需要为项目创建 .gitignore
文件时,只需要打开这个项目,针对你当前所用编程语言或框架,去寻找对应 .gitignore
模板替换即可。
如果你觉得挨个模板查阅很费劲,这里再跟大家推荐一个网站:gitignore.io。
https://www.gitignore.io/
支持一键搜索你所需的 gitignore 模板。
学习如何设计可扩展的系统将会有助于你成为一个更好的工程师。
系统设计是一个很宽泛的话题。在互联网上,关于系统设计原则的资源也是多如牛毛。
这个仓库就是这些资源的组织收集,它可以帮助你学习如何构建可扩展的系统。
GitHub Star:73,100
https://github.com/public-apis/public-apis
这个项目收集了一些可用在 Web 或软件开发的开放 API 接口。
其中包含动画、音乐、书籍、新闻、游戏等多个不同领域的开放 API。
如果你觉得文档看起来不够直观,没关系,小 G 再给你推荐个网站,让你可以直接一键搜索查询开放的 API。
https://public-apis.xyz
GitHub Star:70,100
https://github.com/jlevy/the-art-of-command-line
对于工程师来说,用好命令行能剩下我们不少开发时间,大大解放生产力。
如果你想学好命令行,除了看《鸟哥的私房菜》,还可以看看这个项目。
该项目主要总结一些命令行使用的技巧,内容覆盖面广包括基础、日常使用、文件及数据处理等等,且还给出了具体最常用的例子,无论你是新手还是具有经验的人都值得学习下。
目前该项目已提供多国翻译版本,即使你看英文比较吃力,也不用过于担心。
GitHub 之前爆发过一波热潮:技术路线图。
GitHub Star:64,700
https://github.com/trekhleb/javascript-algorithms
算法与数据结构一直是另工程师颇为头疼的问题。因此,不少工程师在 GitHub 上开放了不少诸如 x-algorithms 的算法与数据结构仓库,目的就是为了帮助大家更好的学习与攻克这些问题的。
下面推荐的这个仓库,从名称上你也可以看出,其实现代码主要还是以 JavaScript 为主。
这个网站里面收集了非常多技术类型的速查表。
你能很轻松的从上面找到具体某项技术的快捷命令与基础语法,用上之后,相信能大幅提升开发效率。
启动memcached时报错:
1 | error while loading shared libraries: libevent-2.1.so.6 |
下面给出解决办法:
1.用ldd命令查看 memcached 命令缺失什么库
1 | [root@Autumn ~]# ldd /usr/local/bin/memcached |
2.在安装libevent时,安装结果告诉我们libevent安装在/usr/local/lib/,可以用locate命令查看:
1 | locate libevent-2.1.so.6 |
如果没有安装locate,请查看:yum安装locate命令。
3.查看 memcached 查找依赖库的路径:
1 | [root@Autumn ~]# LD_DEBUG=libs /usr/local/bin/memcached -v |
发现它查找了search path那一行后面的路径,我们将libevent-2.1.so.6链接到/lib64目录下:
4.链接libevent-2.1.so.6:
1 | sudo ln -s /usr/local/lib/libevent-2.1.so.6 /usr/lib64/libevent-2.1.so.6 |
Android 官方 开发者文档
简单介绍一下Progressive Web App(PWA)
下一代 Web 应用模型 —— Progressive Web App
LAVAS 基于 Vue.js 的 PWA 解决方案
PWA超简单入门
PWA,现代前端必会的黑科技
]]>JAVA的abstract修饰符 && 接口interface用法 && 抽象类和interface的差别
温馨提示:在 IntelliJ IDEA 中有两个 Mac 版本的快捷键,分别为 Mac OS X 和 Mac OS X 10.5+, 其中 Mac OS X 10.5+ 为 IntelliJ IDEA 默认的快捷键版本。此外,建议将 Mac 系统中与 IntelliJ IDEA 冲突的快捷键取消或更改,不建议改 IntelliJ IDEA 的默认快捷键。
⌘
——> Command⇧
——> Shift⌥
——> Option⌃
——> Control↩︎
——> Return/Enter⌫
——> Delete⌦
——> 向前删除键(Fn + Delete)↑
——> 上箭头↓
——> 下箭头←
——> 左箭头→
——> 右箭头⇞
——> Page Up(Fn + ↑
)⇟
——> Page Down(Fn + ↓
)⇥
——> 右制表符(Tab键)⇤
——> 左制表符(Shift + Tab)⎋
——> Escape(Esc)End
——> Fn + →
Home
——> Fn + ←
快捷键 | 作用 |
---|---|
Control + Space | 基本的代码补全(补全任何类、方法、变量) |
Control + Shift + Space | 智能代码补全(过滤器方法列表和变量的预期类型) |
Command + Shift + Enter | 自动结束代码,行末自动添加分号 |
Command + P | 显示方法的参数信息 |
Control + J | 快速查看文档 |
Shift + F1 | 查看外部文档(在某些代码上会触发打开浏览器显示相关文档) |
Command + 鼠标放在代码上 | 显示代码简要信息 |
Command + F1 | 在错误或警告处显示具体描述信息 |
Command + N, Control + Enter, Control + N | 生成代码(getter、setter、hashCode、equals、toString、构造函数等) |
Control + O | 覆盖方法(重写父类方法) |
Control + I | 实现方法(实现接口中的方法) |
Command + Option + T | 包围代码(使用if…else、try…catch、for、synchronized等包围选中的代码) |
Command + / | 注释 / 取消注释与行注释 |
Command + Option + / | 注释 / 取消注释与块注释 |
Option + 方向键上 | 连续选中代码块 |
Option + 方向键下 | 减少当前选中的代码块 |
Control + Shift + Q | 显示上下文信息 |
Option + Enter | 显示意向动作和快速修复代码 |
Command + Option + L | 格式化代码 |
Control + Option + O | 优化 import |
Control + Option + I | 自动缩进线 |
Tab / Shift + Tab | 缩进代码 / 反缩进代码 |
Command + X | 剪切当前行或选定的块到剪贴板 |
Command + C | 复制当前行或选定的块到剪贴板 |
Command + V | 从剪贴板粘贴 |
Command + Shift + V | 从最近的缓冲区粘贴 |
Command + D | 复制当前行或选定的块 |
Command + Delete | 删除当前行或选定的块的行 |
Control + Shift + J | 智能的将代码拼接成一行 |
Command + Enter | 智能的拆分拼接的行 |
Shift + Enter | 开始新的一行 |
Command + Shift + U | 大小写切换 |
Command + Shift + ] / Command + Shift + [ | 选择直到代码块结束 / 开始 |
Option + Fn + Delete | 删除到单词的末尾 |
Option + Delete | 删除到单词的开头 |
Command + 加号 / Command + 减号 | 展开 / 折叠代码块 |
Command + Shift + 加号 | 展开所以代码块 |
Command + Shift + 减号 | 折叠所有代码块 |
Command + W | 关闭活动的编辑器选项卡 |
快捷键 | 作用 |
---|---|
Double Shift | 查询任何东西 |
Command + F | 文件内查找 |
Command + G | 查找模式下,向下查找 |
Command + Shift + G | 查找模式下,向上查找 |
Command + R | 文件内替换 |
Command + Shift + F | 全局查找(根据路径) |
Command + Shift + R | 全局替换(根据路径) |
Command + Shift + S | 查询结构(Ultimate Edition 版专用,需要在 Keymap 中设置) |
Command + Shift + M | 替换结构(Ultimate Edition 版专用,需要在 Keymap 中设置) |
快捷键 | 作用 |
---|---|
Option + F7 / Command + F7 | 在文件中查找用法 / 在类中查找用法 |
Command + Shift + F7 | 在文件中突出显示的用法 |
Command + Option + F7 | 显示用法 |
快捷键 | 作用 |
---|---|
Command + F9 | 编译 Project |
Command + Shift + F9 | 编译选择的文件、包或模块 |
Control + Option + R | 弹出 Run 的可选择菜单 |
Control + Option + D | 弹出 Debug 的可选择菜单 |
Control + R | 运行 |
Control + D | 调试 |
Control + Shift + R, Control + Shift + D | 从编辑器运行上下文环境配置 |
快捷键 | 作用 |
---|---|
F8 | 进入下一步,如果当前行断点是一个方法,则不进入当前方法体内 |
F7 | 进入下一步,如果当前行断点是一个方法,则进入当前方法体内,如果该方法体还有方法,则不会进入该内嵌的方法中 |
Shift + F7 | 智能步入,断点所在行上有多个方法调用,会弹出进入哪个方法 |
Shift + F8 | 跳出 |
Option + F9 | 运行到光标处,如果光标前有其他断点会进入到该断点 |
Option + F8 | 计算表达式(可以更改变量值使其生效) |
Command + Option + R | 恢复程序运行,如果该断点下面代码还有断点则停在下一个断点上 |
Command + F8 | 切换断点(若光标当前行有断点则取消断点,没有则加上断点) |
Command + Shift + F8 | 查看断点信息 |
快捷键 | 作用 |
---|---|
Command + O | 查找类文件 |
Command + Shift + O | 查找所有类型文件、打开文件、打开目录,打开目录需要在输入的内容前面或后面加一个反斜杠 |
Command + Option + O | 前往指定的变量 / 方法 |
Control + 方向键左 / Control + 方向键右 | 左右切换打开的编辑 tab 页 |
F12 | 返回到前一个工具窗口 |
Esc | 从工具窗口进入代码文件窗口 |
Shift + Esc | 隐藏当前或最后一个活动的窗口,且光标进入代码文件窗口 |
Command + Shift + F4 | 关闭活动 run/messages/find/… tab |
Command + L | 在当前文件跳转到某一行的指定处 |
Command + E | 显示最近打开的文件记录列表 |
Option + 方向键左 / Option + 方向键右 | 光标跳转到当前单词 / 中文句的左 / 右侧开头位置 |
Command + Option + 方向键左 / Command + Option + 方向键右 | 退回 / 前进到上一个操作的地方 |
Command + Shift + Delete | 跳转到最后一个编辑的地方 |
Option + F1 | 显示当前文件选择目标弹出层,弹出层中有很多目标可以进行选择(如在代码编辑窗口可以选择显示该文件的 Finder) |
Command + B / Command + 鼠标点击 | 进入光标所在的方法/变量的接口或是定义处 |
Command + Option + B | 跳转到实现处,在某个调用的方法名上使用会跳到具体的实现处,可以跳过接口 |
Option + Space, Command + Y | 快速打开光标所在方法、类的定义 |
Control + Shift + B | 跳转到类型声明处 |
Command + U | 前往当前光标所在方法的父类的方法 / 接口定义 |
Control + 方向键下 / Control + 方向键上 | 当前光标跳转到当前文件的前一个 / 后一个方法名位置 |
Command + ] / Command + [ | 移动光标到当前所在代码的花括号开始 / 结束位置 |
Command + F12 | 弹出当前文件结构层,可以在弹出的层上直接输入进行筛选(可用于搜索类中的方法) |
Control + H | 显示当前类的层次结构 |
Command + Shift + H | 显示方法层次结构 |
Control + Option + H | 显示调用层次结构 |
F2 / Shift + F2 | 跳转到下一个 / 上一个突出错误或警告的位置 |
F4 / Command + 方向键下 | 编辑 / 查看代码源 |
Option + Home | 显示到当前文件的导航条 |
F3 | 选中文件 / 文件夹 / 代码行,添加 / 取消书签 |
Option + F3 | 选中文件 / 文件夹/代码行,使用助记符添加 / 取消书签 |
Control + 0…Control + 9 | 定位到对应数值的书签位置 |
Command + F3 | 显示所有书签 |
快捷键 | 作用 |
---|---|
F5 | 复制文件到指定目录 |
F6 | 移动文件到指定目录 |
Command + Delete | 在文件上为安全删除文件,弹出确认框 |
Shift + F6 | 重命名文件 |
Command + F6 | 更改签名 |
Command + Option + N | 一致性 |
Command + Option + M | 将选中的代码提取为方法 |
Command + Option + V | 提取变量 |
Command + Option + F | 提取字段 |
Command + Option + C | 提取常量 |
Command + Option + P | 提取参数 |
快捷键 | 作用 |
---|---|
Command + K | 提交代码到版本控制器 |
Command + T | 从版本控制器更新代码 |
Option + Shift + C | 查看最近的变更记录 |
Control + C | 快速弹出版本控制器操作面板 |
快捷键 | 作用 |
---|---|
Command + Option + J | 弹出模板选择窗口,将选定的代码使用动态模板包住 |
Command + J | 插入自定义动态代码模板 |
快捷键 | 作用 |
---|---|
Command + 1…Command + 9 | 打开相应编号的工具窗口 |
Command + S | 保存所有 |
Command + Option + Y | 同步、刷新 |
Control + Command + F | 切换全屏模式 |
Command + Shift + F12 | 切换最大化编辑器 |
Option + Shift + F | 添加到收藏夹 |
Option + Shift + I | 检查当前文件与当前的配置文件 |
`Control + `` | 快速切换当前的 scheme(切换主题、代码样式等) |
Command + , | 打开 IDEA 系统设置 |
Command + ; | 打开项目结构对话框 |
Shift + Command + A | 查找动作(可设置相关选项) |
Control + Shift + Tab | 编辑窗口标签和工具窗口之间切换(如果在切换的过程加按上 delete,则是关闭对应选中的窗口) |
]]>全网销量最高的近20款茶评测,买前看这一篇就够了
喝茶选对时间,比吃保健品有用100倍
喝普洱生茶选大益还是中茶还是下关,有木有推荐?
请问有哪些茶企在做普洱中期茶?
花茶搭配养生
根据加工方式和发酵程度的不同,国内现在一般习惯将茶叶分为六大类。
绿茶(不发酵)
黄茶(10-20%发酵度)
白茶(20-30%发酵度)
青茶(30-60%发酵度)
红茶(80-90%发酵度)
黑茶(100%发酵度)
发酵程度越高的茶,茶性就更温和,对胃的刺激性就不大,比如红茶、黑茶就适合胃不太好的人喝;
相反,不发酵或是轻度发酵的茶,茶性偏寒性,适合降火去燥,适合在夏天饮用,但脾胃较弱的人就应该少喝。
六大茶类中,绿茶的全年消费量,是其他五类茶的总和的两倍以上,是我国产量最多的一类茶叶,目前占世界茶叶市场绿茶贸易量的70%左右。
特点功效:茶多酚含量高,有很好的防辐射作用,适合常在电脑前工作的人。
主要类型:蒸青绿茶、晒青绿茶、烘青绿茶、炒青绿茶。
品质特征:具有“清汤绿叶”的品质特点,色泽绿润,内质香气高鲜,汤色绿明,滋味纯和而爽口,富有收敛性,叶底嫩绿明亮。
工艺流程:鲜叶采摘—摊晾—杀青—揉捻(做形)—干燥
冲泡要点:用 80-90度的水。绿茶的重点在与口感的鲜美和鲜嫩的颜色,所以温泡绿茶才是王道,冲泡时,慢慢将茶叶浸润,即可以保持茶汤鲜美,也能延长冲泡次数。
代表产品:安吉白茶、西湖龙井、洞庭碧螺春、安化松针、安吉白茶、六安瓜片、太平猴魁、华顶云雾等。
特点功效:含有大量的消化酶,对脾胃有好处,适合消化不良,食欲不振,少运动的人。
主要类型:黄芽茶、黄小芽和黄大芽。
品质特征:黄茶主要是“色黄、汤黄、叶底黄”,外形金黄色,毫尖显露,芽壮叶肥,汤色橙黄,香气清高,滋味醇厚爽口。
工艺流程:鲜叶采摘—杀青—揉捻—闷黄—干燥
冲泡要点:黄茶芽头嫩,不能使用高温冲泡,用 85度左右的开水,螺旋状冲泡,千万别闷盖子,否则味道会很“涩”。
代表产品:
黄芽茶——湖南岳阳 君山银针、四川的蒙顶黄芽等
黄小茶——湖南岳阳的北港毛尖、湖南宁乡的沩山毛尖、温州黄汤等
黄大茶——安徽的霍山黄大茶、广东大叶青等
传统白茶不炒不揉片状茶,因茸毛不脱,白毫满身而得名。
特点功效:“女人茶”,含有活性酶,能促进脂肪代谢,血糖平衡,清脑明目。适合三高人群,和青少年。
主要类型:白芽茶和白叶茶。
品质特征:茶芽完整,形态自然、白毫显露、香气清鲜、滋味甘醇、持久耐泡。
工艺流程:鲜叶采摘—萎凋(日晒)—干燥。
冲泡要点:淡淡的茶香与绵甜的汤水,一般用85度左右的开水冲泡,若是老白茶的话,可用高温冲泡,回甘更好,绵厚而丰润。
代表产品:白牡丹、白毫银针、贡眉、寿眉等。
青茶属半发酵茶,即制作时适当发酵,使叶片稍有红变,是介于绿茶与红茶之间的一种茶类。它既有绿茶的鲜浓,又有红茶的甜醇。
特点功效:“美容茶”有较好的降血脂、降胆固醇、助消化的功效。适合体型肥胖者的人群。
主要类型:闽北乌龙、闽南乌龙、广东乌龙和台湾乌龙。
品质特征:采用成熟的对口叶为原料,滋味甘醇、香气馥郁,讲究“韵味”,叶底有明显的绿叶红镶边的特征。
工艺流程:鲜叶采摘—萎凋—做青—炒青—揉捻(做形)—干燥
冲泡要点:乌龙茶需要用沸水冲泡。水质、水温、置茶量、冲泡的时间等,都是影响冲泡乌龙茶的重要因素。除了螺旋注水外还可以用定点注水,最好是在杯壁定点,不要直接将水冲在茶叶上,这样可避免出现苦涩感。
条形的单丛茶和岩茶的冲泡要点是即冲即出,而颗粒形乌龙茶时间可以稍微长一点,等茶叶舒展之后再加快出汤速度,而泡到五泡以后,都需要延长时间。
代表产品:
闽北乌龙:武夷岩茶(大红袍、水仙、肉桂、铁罗汉、白鸡冠、水金龟)
闽南乌龙:铁观音、奇兰、黄金桂
广东乌龙:凤凰单枞、凤凰水仙、岭头单枞
台湾乌龙:冻顶乌龙、包种
红茶加工时不经杀青,直接萎凋,使鲜叶失去部分水分,再揉捻,然后发酵,使所含的茶多酚氧化,变成红色的化合物。这种化合物一部分溶于水,一部分不溶于水,而积累在叶片中,从而形成红汤、红叶。
特点功效:所含咖啡碱和芳香物质有利尿作用,茶性温和暖胃,还有舒张血管的作用。适合尿路不畅,胃部不适,心脏病患的人群。
主要类型:小种红茶、功夫红茶和红碎茶。
品质特征:干茶色泽乌润,滋味醇和,汤色红亮鲜明,具有麦芽糖香或焦糖香。
工艺流程:鲜叶采摘—萎凋—-揉捻—发酵—干燥
冲泡要点:开香时水温为 95℃,冲泡时水温为 80℃~85℃。红茶不宜闷泡,注水后尽快出汤,会获得一杯清甜爽口的红茶。使用的盖碗碗口一定要大,散热透气。置茶量可以少一些,让茶叶有充分的透气空间,而不至于闷坏。出汤要滴干净,不要留有水与茶叶接触过久,出完汤把盖子打开散热。
代表产品:
小种红茶——正山小种、烟小种(金骏眉是正山小种茶的顶级品种)
工夫红茶——滇红、祁红、闽红、湖红、宁红
红碎茶——叶茶、碎茶、片茶、末茶
特点功效:能去油腻,减脂降压,黑茶中所含成分能促进淀粉酶解,改善肠道功能,适合肥胖三高及消化功能差的人。
主要类型:湖南黑茶、湖北老青茶、四川边茶、滇桂黑茶。
品质特征:干茶色泽黑褐或黄褐;汤色橙黄、橙红或琥珀色;叶底色泽黄褐、黑褐油亮。口感醇和爽滑;老茶醇厚顺滑,回甘持久明显,有陈香和药香味。
加工工序:鲜叶采摘—杀青—揉捻—渥堆—干燥(黑毛茶的制作工艺),成品黑茶还需要再次加工精制。
冲泡要点: 盖碗冲泡黑茶时,注水沿着盖碗边缘滑下去,不要直接冲到茶叶上,采用环圈注水或螺旋注水都可以,第一泡洗茶出汤要快速,水温控制在93摄氏度较为合适,如果是散茶,水温在90摄氏度较好。泡它时一般是清洗两遍,第2、3泡适当闷10秒,往下即可闷久点出汤饮用。
代表产品:
安化黑茶:茯砖、黑砖、花砖、花卷、天尖等
湖北黑茶:青砖茶
四川边茶:康砖、方茶、圆茶
滇桂黑茶:云南普洱、广西六堡茶
花茶是由茶叶加香花拌和窨制,使茶叶吸附花香而制成。用来窨制花茶的茶叶素坯简称“茶坯”,依据茶坯的种类不同有烘青花茶、炒青花茶、红茶花茶、乌龙茶花茶等等。依据香花种类的不同有茉莉花茶、珠兰花茶、桂花茶、白兰花茶、玫瑰花茶、玳玳花茶、柚子花茶、金银花茶、菊花茶等。
1、张一元茉莉花茶,茉莉花茶又叫茉莉香片,起源于福建福州,一直都是国家的外事礼茶,香气持久,沁人心脾,味道鲜香浓厚,颜色浅黄清透,白色的花朵泡在茶中赏心悦目,茉莉花茶具有安神、解抑郁,健脾理气,抗衰老,提高身体免疫力的功效,张一元茉莉花茶产自福建福州,极为正宗。
2、艺福堂菊花茶,以菊花为原料,经过鲜花采摘、阴干、生晒蒸煮、烘焙工艺制作而成。菊花是我国传统名花,菊花茶具有散风清热、解毒消炎的作用,味道微甘。自唐朝开始人们就开始饮用菊花茶。艺福堂菊花茶产自桐乡杭白,是有名的茶品。
3、玫瑰花茶,很多人不知道,其实玫瑰花不只是一种观赏花,还是一种珍贵的药材,好看的外表下还有着非常高的药用价值,颜值和内涵并存,冲泡后颜色为淡黄色,味道是玫瑰花香,味甘微苦,可以解抑郁、健脾理气、活血散瘀以及调经止痛,对心脑血管,高血压、心脏病以及妇科病人有治疗作用,还可以美容养颜。
4、thin tea花茶,这是一款混合型的花茶,主要成分是茴香籽,荨麻根、玫瑰花瓣、杜松子、蒲公英根等,都是天然成分,其中蒲公英根和玫瑰花瓣还是一种药材,可以清热解毒、美容养颜,配上其他成分一起清肠排毒、加速新陈代谢、抑制食欲。而且不含番泻叶,不会导致人体腹泻,健康减肥瘦身。这款花茶来自澳大利亚,分为早晚两款,根据早晚体质,成分有所改动,早款清肠排毒,晚款抑制食欲。
普洱茶入门知识经典问答
普洱茶知识合集,读完可入门!
普洱茶知识及术语,初学者必看!
关于普洱茶,你应该知道的十件事!
关于普洱茶的这九大常识,你都知道吗?
3分钟看懂普洱茶名山头
茶叶储存三大原则:干燥、避光、无异味。
绿黄茶因为比较鲜嫩,所以还需要密封、低温处理,建议密封放置在冰箱里。
红白黑茶因为发酵成熟,所以在常温环境,保持干燥、避光、无异味即可。
绿茶
降糖效果好,抗癌防衰、抗辐射、延年益寿效果显著。
红茶
咖啡碱含量丰富,具有抗癌效果。还能兴奋大脑中枢神经、强心、利尿、消除疲劳、提高工作效率、抵抗酒精和尼古丁等毒害、减轻支气管和胆管痉挛、调节体温、兴奋呼吸中枢。
黑茶(普洱、茯砖、青砖)
富含茶多糖,强大保健功能是:降血糖、降血脂、防辐射、抗凝血及血栓、增强机体免疫功能、抗氧化、抗动脉粥样硬化、降血压和保护心血管等。
白茶(白毫银针、白牡丹、贡眉、寿眉)
美容抗衰、抗炎清火、降脂减肥、调降血糖、调控尿酸、保护肝脏、抵御病毒。
乌龙茶(大红袍、铁观音)
抗氧化、控制体重、防治心血管疾病、抗糖尿病、抗突变及抑制癌症、抗过敏、抗病原菌及肠道调节等功效。
绿茶,核心工艺是“杀青”,本性寒。体质偏热、胃火盛、精力充沛者饮用绿茶有很好的清火、醒脑、提神之功。绿茶有很好的防辐射效果,对电脑前工作者有大益。
白茶,核心工艺是“萎凋、阳光干燥”和自然存放。茶性由寒转凉及至平和。新茶属性与功效大多接近绿茶,但最明显不同的是绿茶陈放为草,而白茶陈放为宝。及至老白茶,茶性反而更加平和,以适应更多人。
青茶(乌龙茶),茶性寒转平和,由于发酵程度变化跨度太大,但总体是寒向平温转变。核心工艺是“做青”和“焙火”。发酵轻的很接近绿茶,如清香型铁观音,寒性就较大,发酵重的与红茶接近,适应人群更广。
红茶,茶性转温,核心是“发酵”,胃寒、体弱、年龄偏大者都适用,四肢酸懒、手足发凉者饮之更佳,可加奶蜂蜜等调饮,口味更好。
黑茶,茶性转温,核心工艺是“渥堆”。去油腻、解肉毒、降血脂等,保存的好,年份长后口感与疗效更好。
黄茶,茶性有改变,但不是太大,核心工艺是“闷黄”,特别是近些年,传统工艺黄茶由于制作加工较难,人才师傅缺乏,黄茶绿茶化明显。茶性和功效与绿茶相同或很接近,最大区别是口感了,黄茶更醇厚。
早上-红茶
午后-绿茶/乌龙茶
晚上-黑茶
一天中喝茶注意事项:
每天饮茶1—2次,每次茶叶量2—3克、冲泡2—3 分钟、400毫升的饮量是比较适当的。
不宜空腹饮茶,茶入肺腑会冷脾胃。
饭前不宜饮茶,茶水会冲淡胃酸。
不能用茶水服药,茶中鞣酸会影响药效。
因为经过一昼夜的新陈代谢,人体消耗大量的水分,血液的浓度大。饮一杯淡茶水,不仅可以补充水分而且还可以稀释血液,降低血压。特别是老年人,早起后立即饮一杯淡茶水,对健康有利,饮淡茶水是为了防止损伤胃粘膜。
早上适宜喝红茶:人在睡了一夜之后,身体往往处于相对静止的状态,喝红茶则可促进血液循环,同时能够祛除体内寒气,让大脑供血充足。
红茶性质温和,可在每天早上起床后冲泡一杯,在吃过早餐后饮用,也可加入适量牛奶一起饮。需要提醒的是,千万不要空腹喝茶,因为茶叶中含有咖啡因,空腹喝,可令肠道吸收过多的咖啡因,会出现心慌、尿频等不良反应。时间久了,还会影响人体对维生素B的吸收。
下午 3:00 左右喝茶,在这个时间喝茶,对人体能起到调理的作用,增强身体的抵抗力、补养、还能防止感冒,此时喝茶是一天中最重要的,俗称下午茶,对一些 “三高” 人群来说,如果坚持喝下午茶,能起到药物都无法达到的效果。
午后适宜喝青茶或绿茶:通常情况下,人体在中午时分会肝火旺盛,此时饮用绿茶或者青茶可使这一症状得到缓解。
青茶(如铁观音)性甘凉,入肝经,能清肝胆热,化解肝脏毒素,且维生素E含量丰富,能抵抗衰老; 绿茶则入肾经,利水去浊,令排尿顺畅。另外,绿茶中茶多酚含量极高,抗氧化、消炎效果好。
晚上 8:30 左右喝茶。有许多人对晚上喝茶有误解,怕影响睡觉,其实不然,在这个时间是人体免疫系统最活跃的时间,如果能喝上一泡茶,人体会很容易修补和恢复免疫系统,再造细胞等。对一些神经衰落人群,可以选择喝半发酵的温性的铁观音。千万不要喝绿茶,因为绿茶是不发酵茶,对人体有一定的刺激。
晚间适宜喝黑茶:人在吃了三餐之后,身体会积聚一些肥腻之物在消化系统内,倘若晚饭后能够饮用一杯黑茶则有助于分解积聚的脂肪,既暖胃又助消化。
黑茶性质较温纯,不会影响睡眠。黑茶首选云南普洱,不过,普洱的味道有些人可能接受不了,那么可用白茶代替,比如福建寿眉。寿眉入肺经,茶性平和,也不会影响睡眠。
过浓不饮:
浓茶会使人体的“兴奋性”过度增高,会对心血管系统、神经系统等造成不利影响。有心血管疾病者在饮用浓茶后可能会出现心跳过速、心律不齐的现象,易造成病情反复。
睡前不饮:
这一点对于新茶客尤为重要。很多人睡前饮茶后,入睡会变得非常困难,甚至严重影响次日的精神状态,有神经衰弱或失眠症的人要特别注意。
餐前不饮:
进餐前或进餐中少量饮茶并无大碍,但若大量饮茶或饮用过浓的茶,则会影响很多常量元素(如钙等)微量元素(如铁、锌等)的吸收。需要特别注意的是,在喝牛奶或其他奶制品时,不要同时饮茶。因为茶叶中的茶碱和丹宁酸会和奶制品中的钙元素结合成不溶解于水的钙盐,并排出体外,使奶制品的营养价值大为降低。
酒后不饮:
饮酒后,酒中乙醇通过胃肠道进入血液,在肝脏中转化为乙醛,乙醛再转化为乙酸,乙酸再分解成二氧化碳和水排出。酒后饮茶,茶中的茶碱可迅速对肾起到利尿作用,从而促进尚未分解的乙醛过早地进入肾脏。乙醛对肾有较大的刺激作用,会影响肾功能,所以,经常酒后喝浓茶的人易发生肾病。不仅如此,酒中的乙醇对心血管的刺激性很大,而茶同样具有兴奋心脏的作用,两者合二为一,更增强了对心脏的刺激。所以,心脏病患者酒后喝茶危害更大。
新茶不饮:
新茶会刺激胃黏膜,造成肠胃不适,甚至会使病情加重。从营养学角度来讲,太新鲜的茶叶其营养成分不一定是最好的。因为所谓新茶是指采摘下来不足一个月的茶叶,这些茶叶由于没有经过一段时间的放置,存有对身体健康不良影响的物质,如多酚类、醇类、醛类等物质,且没有被完全氧化,如果长时间喝新茶,有可能出现腹泻、腹胀等不舒服的反应。
隔夜茶不饮:
隔夜茶因搁置时间太久,容易受到病源性生物污染,茶水中的复杂成分也易发生变化,饮隔夜茶可导致胃肠疾病。小时候一直被教导“隔夜茶,拉肚子”看来真是自有道理的。
服药不饮:
有些人尤其是爱喝茶的人,会选择用茶水来送药,殊不知茶水中的鞣质可与药物结合而沉淀,会改变药性,阻碍吸收,影响药效,所以,服药应用白开水。这也就日常所说的“茶解药”。
区分四季,过量不饮:
春饮花茶,夏饮绿茶,秋饮青茶,冬饮红茶。春季饮花茶可以散发一冬积存在人体内的寒邪,促进人体阳气发生。绿茶性味苦寒,夏季饮绿茶为佳,可清热、消暑、解毒、止渴、强心。青茶不寒不热,秋季饮青茶能消除体内的余热,恢复津液。冬季饮红茶最为理想,红茶味甘性温,含有丰富的蛋白质,能助消化、补身体,使人体强壮。虽然茶叶中含有多种维生素和氨基酸,对于清油解腻、增强神经兴奋以及消食利尿具有一定的作用,但并不是喝得越多越好,也不是所有的人都适合饮茶。一般来说,每天饮茶一次至两次,每次2~3克茶叶量比较适当。患有神经衰弱、失眠、甲状腺机能亢进、结核病、心脏病、胃病、肠溃疡者不适合饮茶,哺乳期及怀孕妇女和婴幼儿也不宜饮茶。
头茶不饮
茶叶在栽培与加工过程中受到农药等有害物的污染,茶叶表面总有一定的残留,所以,头遍茶有洗涤作用应弃之不喝。
饭后不饮
茶叶中含有大量鞣酸,鞣酸可以与食物中的铁元素发生反应,生成难以溶解的新物质,时间一长引起人体缺铁,甚至诱发贫血症。正确的方法是:餐后一小时再喝茶。
发烧不饮
茶叶中含有茶碱,有升高体温的作用,发烧病人喝茶无异于“火上浇油”。
溃疡病人不饮
茶叶中的咖啡因因可促进胃酸分泌,升高胃酸浓度,诱发溃疡甚至穿孔。
绿茶、乌龙茶汤色透彻,或水清茶绿,或浅黄透绿,天热、心躁之时品饮,给人清凉爽新之感。
有不少好的乌龙茶,特别是陈放佳的乌龙茶,会出现令人愉悦的果酸,中医认为酸入肝经,因此有疏肝理气之功,但脾胃有病症者不宜多饮。而乌龙茶中的武夷岩茶,更是特点鲜明,味重,“令人释躁平矜,怡情悦性”。凤凰单丛茶香气突出,在通窍理气上尤为明显。
红茶花香、蜜香醇厚,味甘性温。甜入脾经,具有补养气血,补充热能,解除疲劳、调和脾胃有好作用。红茶汤色红艳明亮,给人温暖喜悦之感,天气手脚寒凉、情绪低沉之时最宜饮红茶。
黑茶,味苦性温,多年的黑茶木香陈香果香,远年的六堡茶还有怡人的槟榔香。黑茶五行属水,入肾经。如脸黑无光泽,喉咙肿痛,食欲减退,下痢,背脚冰冷,腰痛,精力衰退者,饮此茶为好。
阳虚体质忌寒凉,可饮暖胃暖身的温性茶,如黑茶、重发酵焙火到位的乌龙茶,特别是这些茶的有年份的好茶。
阴虚体质会多感到热渴、干燥,需要多补充水滋润,黄茶、白茶等清爽淡雅,都可以。
气虚体质无力虚汗、呼吸短促、疲劳乏力、抵抗力弱。适宜益脾胃的食物,温和性的茶品适宜。
痰湿体质湿气大,宜除湿排毒,饮淡茶。湿热体质,甘平的乌龙茶适宜。
血瘀体质绿茶、白茶、花茶皆可。
过敏体质可以选些发酵度高、焙火适度的茶品,如浓香型铁观音、武夷岩茶、东方美人茶等。
气郁体质可以香气高雅、通窍芬芳的花茶,安吉白茶,花香度高的凤凰单丛。
平和体质什么茶都是来者不拒,春饮花、夏饮绿、秋饮青、冬饮红、一年四季喝乌龙。
老人和孩童,可以饮茶,但不宜大量饮茶、不宜浓茶。
而性别来说,一般男性多喜饮绿茶、乌龙茶,及生普洱茶,特别是有人说武夷岩茶,称之为“男人喝的茶”,刚猛、气沉、刮肠、攻腻、调络、去毒。
对于上了岁数的女性,建议当以“熟茶”,特别是红茶为主茶品,性温活血、安宫暖身心,适量饮陈年上好乌龙茶、黑茶也是好选择。
女性特殊时期那几天,或孕期,可以饮茶,但前提是根据前面所讲茶性、体质选对茶,同时不宜大量饮茶、不宜饮过浓茶,可以喝茶,宜淡茶。隔年岩茶、红茶都是不错的选择,尽量少饮绿茶。
中国茶, 有六大类。绿茶、黄茶、白茶、乌龙茶、红茶、黑茶,每一种茶都有其特性,且不同的风格,吸引了各自的粉丝群体。
六大茶类,有何特点?
懂得六大茶类的特性后,才能根据它们各自的特点选择冲泡茶具、冲泡水温、出水时间等。
在众多茶器中,一个款名为白瓷盖碗,这可是茶界万金油,不论您冲泡何种茶类,白瓷盖碗均可冲泡。
不过,我们在冲泡时,还是要根据选择出最适合茶类的冲泡茶器。
因茶选择茶器,这一点很重要。若是茶器选择不到位,茶叶的茶性不能得以完美发挥,茶香、茶滋味都将会大打折扣。
选择玻璃杯冲泡白茶和黄茶,便于观察茶叶在水中舒展、游动、变换的过程,同时玻璃杯散热快,不容易闷坏娇嫩的茶叶,不会导致茶汤变色,可泡出鲜爽感十足的茶叶。
选择白瓷盖碗冲泡白茶、乌龙茶、红茶,目的在于感受每一冲风味的变化。
盖碗,是所有茶具里足“无私奉献”的一款,完全为茶叶提供一个舞台,尽情释放茶香,不吸收茶味,能够保证茶汤的原滋原味。
而使用紫砂壶冲泡黑茶,在于紫砂壶可吸收气味,在冲泡时,能起到净化茶香的作用,减少黑茶因后发酵所产生的陈味。
器为茶之父,可见茶器的选择对一款茶叶的影响之大。
水温的选择,请牢记一个原则:好茶不怕沸水泡。
但为了让茶汤的口感更好喝,我们可以根据自己的口感适当调整。
有的茶友,偏爱用沸水冲泡后的口感,滋味浓郁,茶香足。
而有的茶友冲泡,怕自己出水拿捏不到位,反倒影响了茶汤的口感,所以会稍稍地降低冲泡温度。
不论选择何种冲泡方式,主要目的是为了让茶汤更好喝,这点原则不可动摇。
具体冲泡温度,可根据个人需求调整。
水质,从很大程度上决定决定了茶汤的滋味和口感。
张大复在《梅花草堂笔谈》中说:“茶性必发于水,八分之茶,遇十分之水,茶亦十分矣;八分之水,试十分之茶,茶只八分耳。”可见水对茶的重要性。
陆羽《茶经》有云:“其水,山水上,江水中,井水下。”也说明泡茶用水十分讲究。
如今,我们身边可接触到的冲泡用水,可分为四大类。
在这些水质中,首先把自来水淘汰,不到万不得已的情况下,冲泡茶叶不要用自来水,会严重影响茶叶品质。
优选山泉水,如果没有这类资源,选择矿泉水和纯净水也是可以的。
不同茶类的出水时间,也要掌握到位。
用玻璃杯冲泡绿茶和黄茶,一般等到茶汤凉了之后就可以饮用,用盖碗冲泡茶类则要注意,选择快出水。
不论是白茶、乌龙茶还是红茶,为必满茶汤滋味过浓,出现苦涩味,在冲泡时请遵循快出水的原则。
紫砂壶冲泡黑茶,因出水口本身就不大,故而在出水速度方面也会比较慢,属于慢出水一类。
泡茶,有一个细节不可错过,那就是投茶量的选择。
投茶量,往往会被忽视,有茶友随性,直接用手抓取一些,丢进茶器中,注入沸水,倒出茶汤。
这类比较随性的做法,往往会让茶汤的味道变得不可把控。要么茶汤太浓,要么茶汤太淡,无法准确感知茶叶的滋味和香气。
如用玻璃杯冲泡绿茶和黄茶,一般注入300毫升的水,搭配3-4克左右茶叶即可,不宜过多,长时间浸泡下,容易使茶汤苦涩。
用白瓷盖碗泡茶,也要注意茶水比例。
目前,比较经常使用的,是110-120毫升左右的盖碗。
在科学投茶量下,才可感受到茶叶的香与水,不至于茶汤不是太浓就是太淡。
最后想说的一点,是关于选择茶叶。
中国茶,有六大类之多,这些茶,各有特色,该怎么选?
最重要的一点,是根据体质选茶。
在中医范畴,人的体质可分为九大类,不同体质适合喝的茶类不同。
根据体质选茶,落实到实处,还是要根据茶性选择。
9 种不同体质该如何喝茶
体质 | 主要体征 | 茶类 |
---|---|---|
阴虚体质 | 面颊潮红或偏红,易长斑,眼睛干涩, 口干咽燥,容易失眠,经常大便干结。 | 绿茶、白茶、黄茶 |
气虚体质 | 容易感冒,常出虚汗,体质虚弱, 不耐受寒邪、风邪、暑邪。 | 乌龙茶、白茶、红茶 |
阳虚体质 | 手脚发凉,冬不耐受寒冷, 夏不耐受空调冷气 | 乌龙茶、黑茶、老白茶、红茶 |
特禀体质 | 容易过敏,皮肤容易起荨麻疹 特禀体质的人会出现打喷嚏、流清涕等症状, 是因为卫气虚损不能抵御外邪所致。 中医认为,“肾为先天之本” “脾为后天之本”, 特禀质养生以健脾、补肾气为主。 | 白茶 |
血瘀体质 | 面色晦暗或色素沉着、黄褐色斑块, 眼眶暗黑,易烦躁,健忘,性情急躁。 | 红茶、武夷岩茶、老白茶 |
痰湿体质 | 汗多而黏腻,手足心潮湿多汗, 常感到肢体酸困沉重、容易困倦、不轻松 | 红茶、黑茶、白茶 |
湿热体质 | 面部和鼻尖总是油光发亮, 易生粉刺、常感到口苦、口臭 | 白茶、黑茶 |
气郁体质 | 常感到闷闷不乐、情绪低沉, 唉声叹气、紧张心悸、焦虑 | 绿茶、红茶、乌龙茶 |
平和体质 | 正常体质、理想状态各方面体质好 | 六大茶类均可饮用 |
依据中医的基本理论,各种药物都具有各自最基本的功能特性(也就是“药性”),中医范畴,可将药性分为寒、凉、温、热四种。在四性之外,还有一类叫平性药。
寒凉与温热是相对立的两种药性,寒凉药材多具有清热泻火的功效,适用于热性病症,如喉咙痛的时候就需要喝寒凉类的药物降火。
温热药材一般都具有温里散寒、补火助阳等功效,适用于寒性病症,如四肢厥冷、面色苍白等。
茶叶的“茶性”其实就源于中医的四性。
绿茶、黄茶性寒凉——部分消化系统不好的茶友,不适宜喝寒凉的茶,因为会刺激肠胃。如果是肝火旺,易上火的人,适合喝这类茶。
老白茶、陈年黑茶、红茶,茶性温和,就适合手脚冰凉的人喝。
故而,茶类的选择,还是要根据自己的体质选择。
]]>快捷键 | 描述 |
---|---|
ctrl + a | 到行首 |
ctrl + e | 行末 |
ctrl + u | 清除当前行 |
ctrl + f/b | 前进后退,相当于左右方向键,但是显然比移开手按方向键更快 |
ctrl + p | 上一条命令,相当于方向键上 |
ctrl + r | 搜索命令历史,这个大家都应该很熟悉了 |
ctrl + d | 删除当前字符 |
ctrl + h | 删除之前的字符 |
ctrl + w | 删除光标前的单词 |
ctrl + k | 删除到文本末尾 |
ctrl + t | 交换光标处文本 |
⌘ + —/+/0 | 调整字体大小 |
⌘ + r | 清屏,其实是滚到新的一屏,并没有清空。ctrl + l 也可以做到 |
快捷键 | 描述 |
---|---|
command + t | 新建标签 |
command + w | 关闭标签 |
command + 数字 command + 左右方向键 | 切换标签 |
command + enter | 切换全屏 |
command + f | 查找 |
快捷键 | 描述 |
---|---|
command + d | 垂直分屏 |
command + shift + d | 水平分屏 |
command + option + 方向键 或者 command + [ ] | 切换屏幕 |
command + ; | 查看历史命令 |
command + shift + h | 查看剪贴板历史 |
快捷键 | 描述 |
---|---|
⌘ + 数字 | 在各 tab 标签直接来回切换 |
选择即复制 + 鼠标中键粘贴 | 这个很实用 |
⌘ + f | 所查找的内容会被自动复制 |
输入开头命令后 按 ⌘ + ; | 会自动列出输入过的命令 |
]]>mac:高效shell终端设置
zsh+on-my-zsh配置教程指南(程序员必备)
我的职业是架构师:12年经验带你入门
软件架构入门 – 阮一峰
成为1个架构师的入门到进阶之路(学习路线图)
]]>架构师之路
W3Cschool架构师之路
网站地址
一个轻量的工具集合,里面包含有媒体类,图片类,文字处理类,编程开发类,日常实用类工具,基本覆盖了我们所有的需求,登录后还有更多的隐藏功能。
网站地址
一个工具箱,里面包含了143个在线工具,包括加密解密,文字编辑,编程开发,单位换算,日期时间,图形图像,金融理财,生活日常等各个门类,应该算是很全面了。
网站地址
程序员必备,里面包含各种常用的开发工具。
网站地址
前端程序员必备,收集了大量高质量的前端相关资源。
]]>文章参考汇总至雷神笔记
最近正在研究H.264和HEVC的编码方式,因此分析了一下最常见的H.264编码器——x264的源代码。本文简单梳理一下它的结构。X264的源代码量比较大而且涉及到很多的算法,目前还有很多不懂的地方,因此也不能保证分析的完全正确。目前打算先把已经理解的部分整理出来以作备忘。
下面解释一下图中关键标记的含义。
函数在图中以方框的形式表现出来。不同的背景色标志了该函数不同的作用:
白色背景的函数:不加区分的普通内部函数。
浅红背景的函数:libx264类库的接口函数(API)。
粉红色背景函数:滤波函数(Filter)。用于环路滤波,半像素插值,SSIM/PSNR的计算。
黄色背景函数:分析函数(Analysis)。用于帧内预测模式的判断,或者帧间预测模式的判断。
绿色背景的函数:宏块编码函数(Encode)。通过对残差的DCT变换、量化等方式对宏块进行编码。
紫色背景的函数:熵编码函数(Entropy Coding)。对宏块编码后的数据进行CABAC或者CAVLC熵编码。
蓝色背景函数:汇编函数(Assembly)。做过汇编优化的函数。图中主要画出了这些函数的C语言版本,此外这些函数还包含MMX版本、SSE版本、NEON版本等。
浅蓝色背景函数:码率控制函数(Rate Control)。对码率进行控制的函数。具体的方法包括了ABR、CBR、CRF等。
整个关系图可以分为以下几个区域:
箭头线标志了函数的调用关系:
每个函数标识了它所在的文件路径。
下文简单记录图中几个关键的部分。
x264命令行程序指的是x264项目提供的控制台程序。通过这个程序可以调用libx264编码YUV为H.264码流。该程序的入口函数为 main()
。main()
函数首先调用 parse()
解析输入的参数,然后调用 encode()
编码YUV数据。
parse()首先调用 x264_param_default()
为保存参数的 x264_param_t
结构体赋默认值;然后在一个大循环中通过 getopt_long()
解析通过命令行传递来的存储在 argv[]
中的参数,并作相应的设置工作;最后调用 select_input()
和 select_output()
完成输入文件格式(yuv,y4m等)和输出文件格式(裸流,mp4,mkv,FLV等)的设置。
encode()首先调用 x264_encoder_open()
打开编码器;接着在一个循环中反复调用 encode_frame()
一帧一帧地进行编码;最后在编码完成后调用 x264_encoder_close()
关闭编码器。
encode_frame()则调用 x264_encoder_encode()
将存储YUV数据的 x264_picture_t
编码为存储H.264数据的 x264_nal_t
。
在一个x264编码流程中,至少需要调用如下API函数(参考文章《最简单的视频编码器:基于libx264(编码YUV为H.264)》):
1 | x264_param_default() // 设置参数集结构体x264_param_t的缺省值。 |
libx264主干函数指的是编码API之后,x264_slice_write()
之前的函数。这一部分函数较多,暂时不详细分析,仅仅举几个例子列一下它们的功能。
1 | x264_encoder_open() // 调用了下面的函数: |
x264_encoder_headers()
调用了下面的函数:
1 | x264_sps_write()// 输出SPS |
x264_encoder_encode()调用了下面的函数:
1 | x264_frame_pop_unused()// 获取1个x264_frame_t类型结构体fenc。如果frames.unused[]队列不为空,就调用x264_frame_pop()从unused[]队列取1个现成的;否则就调用x264_frame_new()创建一个新的。 |
x264_slice_write()
用于编码 Slice。该函数中包含了一个很长的 for()
循环。该循环每执行一遍编码一个宏块。x264_slice_write()
中以下几个函数比较重要:
1 | x264_nal_start() // 开始写一个NALU。 |
滤波模块对应的函数是 x264_fdec_filter_row()
。该函数完成了环路滤波,半像素插值,SSIM/PSNR
的计算的功能。该函数调用了以下及个比较重要的函数:
1 | x264_frame_deblock_row()// 去块效应滤波器。 |
分析模块对应的函数是 x264_macroblock_analyse()
。该函数包含了帧内预测模式分析以及帧间运动估计等。该函数调用了以下比较重要的函数(只列举了几个有代表性的函数):
1 | x264_mb_analyse_init()// Analysis模块初始化。 |
宏块编码模块对应的函数是 x264_macroblock_encode()
。该模块通过对残差的 DCT 变换、量化等方式对宏块进行编码。对于 Intra16x16
宏块,调用 x264_mb_encode_i16x16()
进行编码,对于 Intra4x4
,调用 x264_mb_encode_i4x4()
进行编码。对于Inter类型的宏块则直接在函数体里面编码。
CABAC 熵编码对应的函数是 x264_macroblock_write_cabac()
。CAVLC 熵编码对应的函数是 x264_macroblock_write_cavlc()
。x264_macroblock_write_cavlc()
调用了以下几个比较重要的函数:
1 | x264_cavlc_mb_header_i()// 写入I宏块MB Header数据。包含帧内预测模式等。 |
码率控制模块函数分布在x264源代码不同的地方,包含了以下几个比较重要的函数:
1 | x264_encoder_open() 中的 x264_ratecontrol_new()// 创建码率控制。 |
该命令行工具可以调用 libx264 将 YUV 格式像素数据编码为 H.264 码流。
从图中可以看出,X264命令行工具调用了libx264的几个API完成了H.264编码工作。使用libx264的API进行编码可以参考《最简单的视频编码器:基于libx264(编码YUV为H.264)》,这个流程中最关键的API包括:
1 | x264_param_default()// 设置参数集结构体x264_param_t的缺省值。 |
在X264命令行工具中,main()
首先调用 parse()
解析输入的命令行参数,然后调用 encode()
进行编码。
parse()
首先调用 x264_param_default()
为存储参数的结构体 x264_param_t
赋默认值;然后在一个大循环中调用 getopt_long()
逐个解析输入的参数,并作相应的处理;最后调用 select_input()
和 select_output()
解析输入文件格式(例如yuv,y4m…)和输出文件格式(例如raw,flv,MP4…)。
encode()
首先调用 x264_encoder_open()
打开H.264编码器,然后调用 x264_encoder_headers()
输出H.264码流的头信息(例如SPS、PPS、SEI),接着进入一个循环并且调用 encode_frame()
逐帧编码视频,最后调用 x264_encoder_close()
关闭解码器。其中 encode_frame()
中又调用了 x264_encoder_encode()
完成了具体的编码工作。下文将会对上述流程展开分析。
main()
的定义很简单,它主要调用了两个函数:parse()
和 encode()
。main()
首先调用 parse()
解析输入的命令行参数,然后调用 encode()
进行编码。下面分别分析这两个函数。
parse()
用于解析命令行输入的参数(存储于 argv[]
中)
下面简单梳理 parse()
的流程:
(1)调用 x264_param_default()
为存储参数的结构体 x264_param_t
赋默认值
(2)调用 x264_param_default_preset()
为 x264_param_t
赋值
(3)在一个大循环中调用 getopt_long()
逐个解析输入的参数,并作相应的处理。举几个例子:
help()
打开帮助菜单。print_version_info()
打印版本信息。x264_param_parse()
进行处理。(4)调用 select_input()
解析输出文件格式(例如raw,flv,MP4…)
(5)调用 select_output()
解析输入文件格式(例如yuv,y4m…)
下文按照顺序记录parse()中涉及到的函数:
1 | x264_param_default() |
x264_param_default()
是一个x264的API。该函数用于设置x264中 x264_param_t
结构体的默认值。
x264_param_default_preset()
是一个 libx264 的 API,用于设置 x264 的 preset 和 tune。
从源代码可以看出,x264_param_default_preset()
调用 x264_param_apply_preset()
设置 preset,调用 x264_param_apply_tune()
设置 tune。记录一下这两个函数。
help()
用于打印帮助菜单。在 x264 命令行程序中添加 “-h” 参数后会调用该函数。
print_version_info()
用于打印 x264 的版本信息。在x264命令行程序中添加 “-V” 参数后会调用该函数。
x264_param_parse()
是一个 x264 的 API。该函数以字符串键值对的方式设置 x264_param_t
结构体的一个成员变量。
x264_param_parse()
中判断参数的宏 OPT()
和 OPT2()
实质上就是 strcmp()
。由此可见该函数的流程首先是调用 strcmp()
判断当前输入参数的名称 name,然后再调用 atoi()
,atof()
,或者 atobool()
等将当前输入参数值 value 转换成相应类型的值并赋值给对应的参数。
x264_param_apply_profile()
是一个 x264 的 API。该函数用于设置 x264 的 profile
select_output()
用于设定输出的文件格式。
select_input()
用于设定输入的文件格式。
encode()
编码 YUV 为 H.264 码流
从源代码可以梳理出来 encode()
的流程:
(1)调用 x264_encoder_open()
打开 H.264 编码器。
(2)调用 x264_encoder_parameters()
获得当前的参数集 x264_param_t
,用于后续步骤中的一些配置。
(3)调用输出格式(H.264裸流、FLV、mp4等)对应 cli_output_t
结构体的 set_param()
方法,为输出格式的封装器设定参数。其中参数源自于上一步骤得到的 x264_param_t
。
(4)如果不是在每个keyframe前面都增加 SPS/PPS/SEI 的话,就调用 x264_encoder_headers()
在整个码流前面加 SPS/PPS/SEI。
(5)进入一个循环中进行一帧一帧的将 YUV 编码为 H.264:
cli_vid_filter_t
结构体 get_frame()
方法,获取一帧YUV数据。encode_frame()
编码该帧YUV数据为H.264数据,并且输出出来。该函数内部调用x264_encoder_encode()
完成编码工作,调用输出格式对应 cli_output_t
结构体的 write_frame()
完成了输出工作。cli_vid_filter_t
结构体 release_frame()
方法,释放刚才获取的 YUV 数据。print_status()
输出一些统计信息。(6)编码即将结束的时候,进入另一个循环,输出编码器中缓存的视频帧:
encode_frame()
,将编码器中缓存的剩余几帧数据编码输出出来。print_status()
输出一些统计信息。(7)调用 x264_encoder_close()
关闭 H.264 编码器。
encode()
的流程中涉及到 libx264 的几个关键的 API:
1 | x264_encoder_open()// 打开H.264编码器。 |
此外上述流程中涉及到两个比较简单的函数:encode_frame()
和 print_status()
。其中 encode_frame()
用于编码一帧数据,而 print_status()
用于输出一帧数据编码后的统计信息。下文记录一下这两个函数的定义。
encode_frame()
内部调用 x264_encoder_encode()
完成编码工作,调用输出格式对应 cli_output_t
结构体的 write_frame()
完成了输出工作。
print_status()的代码不再详细记录,它的输出效果如下图中红框中的文字。
在x264控制台程序中有3个和输入输出相关的结构体:
1 | cli_output_t// 输出格式对应的结构体。输出格式一般为H.264裸流、FLV、MP4等。 |
在 x264 的编码过程中,调用 cli_vid_filter_t
结构体的 get_frame()
读取 YUV 数据,调用 cli_output_t
的 write_frame()
写入数据。
“主干部分”指的就是libx264中最核心的接口函数—— x264_encoder_encode()
,以及相关的几个接口函数x264_encoder_open()
,x264_encoder_headers()
,和 x264_encoder_close()
。
从图中可以看出,x264 主干部分最复杂的函数就是 x264_encoder_encode()
,该函数完成了编码一帧 YUV 为H.264 码流的工作。与之配合的还有打开编码器的函数 x264_encoder_open()
,关闭编码器的函数 x264_encoder_close()
,以及输出 SPS/PPS/SEI 这样的头信息的 x264_encoder_headers()
。
x264_encoder_open()
用于打开编码器,其中初始化了 libx264 编码所需要的各种变量。它调用了下面的函数:
1 | x264_validate_parameters()// 检查输入参数(例如输入图像的宽高是否为正数)。 |
x264_encoder_headers()
输出 SPS/PPS/SEI 这些 H.264 码流的头信息。它调用了下面的函数:
1 | x264_sps_write()// 输出SPS |
x264_encoder_encode()
编码一帧 YUV 为 H.264 码流。它调用了下面的函数:
1 | x264_frame_pop_unused()// 获取1个x264_frame_t类型结构体fenc。如果frames.unused[]队列不为空,就调用x264_frame_pop()从unused[]队列取1个现成的;否则就调用x264_frame_new()创建一个新的。 |
x264_encoder_close()
用于关闭解码器,同时输出一些统计信息。它调用了下面的函数:
1 | x264_lookahead_delete()// 释放Lookahead相关的变量。 |
x264_encoder_open()
是一个 libx264 的 API。该函数用于打开编码器,其中初始化了 libx264 编码所需要的各种变量。
根据函数调用的顺序,看一下 x264_encoder_open()
调用的下面几个函数:
1 | x264_sps_init()// 根据输入参数生成H.264码流的SPS信息。 |
简单记录一下帧内预测的方法。帧内预测根据宏块左边和上边的边界像素值推算宏块内部的像素值,帧内预测的效果如下图所示。其中左边的图为图像原始画面,右边的图为经过帧内预测后没有叠加残差的画面。
H.264 中有两种帧内预测模式:16x16
亮度帧内预测模式和 4x4
亮度帧内预测模式。其中 16x16
帧内预测模式一共有 4 种,如下图所示。
这 4 种模式列表如下。
模式 | 描述 |
---|---|
Vertical | 由上边像素推出相应像素值 |
Horizontal | 由左边像素推出相应像素值 |
DC | 由上边和左边像素平均值推出相应像素值 |
Plane | 由上边和左边像素推出相应像素值 |
4x4
帧内预测模式一共有 9 种,如下图所示。
简单记录几个像素计算中的概念。SAD 和 SATD 主要用于帧内预测模式以及帧间预测模式的判断。有关 SAD、SATD、SSD 的定义如下:
SAD(Sum of Absolute Difference)也可以称为SAE(Sum of Absolute Error),即绝对误差和。它的计算方法就是求出两个像素块对应像素点的差值,将这些差值分别求绝对值之后再进行累加。
SATD(Sum of Absolute Transformed Difference)即Hadamard变换后再绝对值求和。它和SAD的区别在于多了一个“变换”。
SSD(Sum of Squared Difference)也可以称为SSE(Sum of Squared Error),即差值的平方和。它和SAD的区别在于多了一个“平方”。
H.264中使用SAD和SATD进行宏块预测模式的判断。早期的编码器使用SAD进行计算,近期的编码器多使用SATD进行计算。为什么使用SATD而不使用SAD呢?关键原因在于编码之后码流的大小是和图像块DCT变换后频域信息紧密相关的,而和变换前的时域信息关联性小一些。SAD只能反应时域信息;SATD却可以反映频域信息,而且计算复杂度也低于DCT变换,因此是比较合适的模式选择的依据。
使用SAD进行模式选择的示例如下所示。下面这张图代表了一个普通的 Intra16x16
的宏块的像素。它的下方包含了使用Vertical,Horizontal,DC和Plane四种帧内预测模式预测的像素。通过计算可以得到这几种预测像素和原始像素之间的SAD(SAE)分别为3985,5097,4991,2539。由于Plane模式的SAD取值最小,由此可以断定Plane模式对于这个宏块来说是最好的帧内预测模式。
简单记录一下DCT相关的知识。DCT变换的核心理念就是把图像的低频信息(对应大面积平坦区域)变换到系数矩阵的左上角,而把高频信息变换到系数矩阵的右下角,这样就可以在压缩的时候(量化)去除掉人眼不敏感的高频信息(位于矩阵右下角的系数)从而达到压缩数据的目的。二维 8x8
DCT变换常见的示意图如下所示。
早期的DCT变换都使用了 8x8
的矩阵(变换系数为小数)。在 H.264 标准中新提出了一种 4x4
的矩阵。这种 4x4
DCT变换的系数都是整数,一方面提高了运算的准确性,一方面也利于代码的优化。4x4
整数DCT变换的示意图如下所示(作为对比,右侧为 4x4
块的Hadamard变换的示意图)。
简单记录一下半像素插值的知识。《H.264标准》中规定,运动估计为 1/4
像素精度。因此在H.264编码和解码的过程中,需要将画面中的像素进行插值——简单地说就是把原先的 1 个像素点拓展成 4x4
一共16个点。下图显示了H.264编码和解码过程中像素插值情况。可以看出原先的 G 点的右下方通过插值的方式产生了a、b、c、d等一共 16 个点。
如图所示,1/4
像素内插一般分成两步:
(1)半像素内插。这一步通过 6 抽头滤波器获得 5 个半像素点。
(2)线性内插。这一步通过简单的线性内插获得剩余的 1/4
像素点。
图中半像素内插点为 b、m、h、s、j 五个点。半像素内插方法是对整像素点进行 6 抽头滤波得出,滤波器的权重为( 1/32, -5/32, 5/8, 5/8, -5/32, 1/32
)。例如 b 的计算公式为:
b=round( (E - 5F + 20G + 20H - 5I + J ) / 32)
剩下几个半像素点的计算关系如下:
1 | m:由B、D、H、N、S、U计算 |
在获得半像素点之后,就可以通过简单的线性内插获得 1/4
像素内插点了。1/4
像素内插的方式如下图所示。例如图中 a 点的计算公式如下:
A=round( (G+b)/2 )
在这里有一点需要注意:位于 4 个角的e、g、p、r 四个点并不是通过 j 点计算计算的,而是通过b、h、s、m四个半像素点计算的。
x264_encoder_headers()
是libx264的一个API函数,用于输出 SPS/PPS/SEI 这些 H.264 码流的头信息。
x264_encoder_close()
是libx264的一个API函数。该函数用于关闭编码器,同时输出一些统计信息。
x264_encoder_encode()
是libx264的API函数,用于编码一帧 YUV 为 H.264 码流。
x264_encoder_encode()
的流程大致如下:
(1)调用 x264_frame_pop_unused
获取一个空的 fenc
(x264_frame_t类型)用于存储一帧编码像素数据。
(2)调用 x264_frame_copy_picture()
将外部结构体的 pic_in
(x264_picture_t
类型)的数据拷贝给内部结构体的 fenc
(x264_frame_t
类型)。
(3)调用 x264_lookahead_put_frame()
将 fenc
放入 Lookahead 模块的队列中,等待确定帧类型。
(4)调用 x264_lookahead_get_frames()
分析 Lookahead 模块中一个帧的帧类型。分析后的帧保存在frames.current[]
中。
(5)调用 x264_frame_shift()
从 frames.current[]
中取出分析帧类型之后的 fenc
。
(6)调用 x264_reference_update()
更新参考帧队列 frames.reference[]
。
(7)如果编码帧 fenc
是 IDR
帧,调用 x264_reference_reset()
清空参考帧队列 frames.reference[]
。
(8)调用 x264_reference_build_list()
创建参考帧列表 List0
和 List1
。
(9)根据选项做一些配置:
b_aud
不为 0,输出 AUD 类型 NALUb_repeat_headers
不为 0,调用 x264_sps_write()
和 x264_pps_write()
输出 SPS 和 PPS。(10)调用 x264_slice_init()
初始化 Slice Header 信息。
(11)调用 x264_slices_write()
进行编码。该部分是 libx264 的核心,在后续文章中会详细分析。
(12)调用 x264_encoder_frame_end()
做一些编码后的后续处理。
x264_slice_write()
是完成编码工作的函数。该函数中包含了去块效应滤波,运动估计,宏块编码,熵编码等模块。
x264_slice_write()
是 x264 项目的核心,它完成了编码了一个 Slice 的工作。根据功能的不同,该函数可以分为滤波(Filter),分析(Analysis),宏块编码(Encode)和熵编码(Entropy Encoding)几个子模块。
x264_slice_write()调用了如下函数:
1 | x264_nal_start()// 开始写一个NALU。 |
根据源代码简单梳理了 x264_slice_write()
的流程,如下所示:
(1)调用 x264_nal_start()
开始输出一个 NALU。
(2)x264_macroblock_thread_init()
:初始化宏块重建像素缓存 fdec_buf[]
和编码像素缓存 fenc_buf[]
。
(3)调用 x264_slice_header_write()
输出 Slice Header。
(4)进入一个循环,该循环每执行一遍编码一个宏块:
x264_fdec_filter_row()
执行滤波模块。x264_macroblock_cache_load_progressive()
将要编码的宏块的周围的宏块的信息读进来。x264_macroblock_analyse()
执行分析模块。x264_macroblock_encode()
执行宏块编码模块。x264_macroblock_write_cabac()/x264_macroblock_write_cavlc()
执行熵编码模块。x264_macroblock_cache_save()
保存当前宏块的信息。x264_ratecontrol_mb()
执行码率控制。(5)调用 x264_nal_end()
结束输出一个 NALU。
X264在宏块编码方面涉及到下面几个比较重要的结构体:
宏块像素存储缓存 fenc_buf[]
和 fdec_buf[]
——位于 x264_t.mb.pic
中,用于存储宏块的亮度和色度像素。
宏块各种信息的缓存 Cache——位于 x264_t.mb.pic
中,用于存储宏块的信息例如 4x4
帧内预测模式、DCT 的非 0 系数个数、运动矢量、参考帧序号等。
图像半像素点存储空间 filtered[]
——位于 x264_frame_t
中,用于存储半像素插值后的点。
fenc_buf[]
和 fdec_buf[]
为 x264_t.mb.cache
中的结构体,用于存储一个宏块的像素数据。其中 fenc_buf[]
用于存储宏块编码像素数据,而 fdec_buf[]
用于存储宏块重建像素数据。他们的定义如下所示。
1 | /* space for p_fenc and p_fdec */ |
从定义可以看出,fenc_buf[]
每行 16 个数据;而 fdec_buf[]
每行 32 个数据。在 x264_t.mb.cache
中和 fenc_buf[]
和 fdec_buf[]
相关的指针数组还有 p_fenc[3]
和 p_fdec[3]
,它们中的 3 个元素 [0]、[1]、[2]
分别指向分别指向对应缓存 buf 的 Y、U、V 分量。下图画出了像素格式为 YUV420P 的时候 fenc_buf[]
的存储示意图。图中灰色区域存储 Y,蓝色区域存储 U,粉红区域存储 V。p_fenc[0]
指向 Y 的存储区域,p_fenc[1]
指向 U 的存储区域,p_fenc[2]
指向 V 的存储区域,在图中以方框的形式标注了出来。
下图画出了像素格式为 YUV420P 的时候 fdec_buf[]
的存储示意图。图中灰色区域存储 Y,蓝色区域存储 U,粉红区域存储 V。p_fenc[0]
指向 Y 的存储区域,p_fenc[1]
指向 U 的存储区域,p_fenc[2]
指向 V 的存储区域,在图中以方框的形式标注了出来。
从图中可以看出,fdec_buf[]
和 fenc_buf[]
主要的区别在于 fdec_buf[]
像素块的左边和上边包含了左上方相邻块用于预测的像素。
在 x264 中 x264_t.mb.cache
结构体中包含了存储宏块信息的各种各样的缓存 Cache。例如:
Intra4x4
帧内预测模式的缓存x264_fdec_filter_row()
对应着 x264 中的滤波模块。滤波模块主要完成了下面 3 个方面的功能:
(1)环路滤波(去块效应滤波)
(2)半像素内插
(3)视频质量指标PSNR和SSIM的计算
从图中可以看出,滤波模块对应的x264_fdec_filter_row()调用了如下函数:
1 | x264_frame_deblock_row()// 去块效应滤波器。 |
从源代码可以看出,x264_fdec_filter_row()
完成了三步工作:
(1)环路滤波(去块效应滤波)。通过调用 x264_frame_deblock_row()
实现。
(2)半像素内插。通过调用 x264_frame_filter()
实现。
(3)视频质量 SSIM 和 PSNR 计算。PSNR在这里只计算了 SSD,通过调用 x264_pixel_ssd_wxh()
实现;SSIM 的计算则是通过 x264_pixel_ssim_wxh()
实现。
x264_macroblock_analyse()
对应着 x264 中的分析模块。分析模块主要完成了下面 2 个方面的功能:
(1)对于帧内宏块,分析帧内预测模式
(2)对于帧间宏块,进行运动估计,分析帧间预测模式
从图中可以看出,分析模块的 x264_macroblock_analyse()
调用了如下函数(只列举了几个有代表性的函数):
1 | x264_mb_analyse_init()// Analysis模块初始化。 |
尽管 x264_macroblock_analyse()
的源代码比较长,但是它的逻辑比较清晰,如下所示:
(1)如果当前是 I
Slice,调用 x264_mb_analyse_intra()
进行 Intra 宏块的帧内预测模式分析。
(2)如果当前是 P
Slice,则进行下面流程的分析:
x264_macroblock_probe_pskip()
分析是否为 Skip 宏块,如果是的话则不再进行下面分析。x264_mb_analyse_inter_p16x16()
分析 P16x16
帧间预测的代价。x264_mb_analyse_inter_p8x8()
分析 P8x8
帧间预测的代价。P8x8
代价值小于 P16x16
,则依次对 4 个 8x8
的子宏块分割进行判断:x264_mb_analyse_inter_p4x4()
分析 P4x4
帧间预测的代价。P4x4
代价值小于 P8x8
,则调用 x264_mb_analyse_inter_p8x4()
和x264_mb_analyse_inter_p4x8()
分析 P8x4
和 P4x8
帧间预测的代价。P8x8
代价值小于 P16x16
,调用 x264_mb_analyse_inter_p16x8()
和x264_mb_analyse_inter_p8x16()
分析 P16x8
和 P8x16
帧间预测的代价。x264_mb_analyse_intra()
,检查当前宏块作为 Intra 宏块编码的代价是否小于作为 P
宏块编码的代价(P
Slice中也允许有 Intra 宏块)。(3)如果当前是 B
Slice,则进行和 P
Slice类似的处理。
总体说来 x264_mb_analyse_intra()
通过计算 Intra16x16
,Intra8x8
(暂时没有研究),Intra4x4
这 3 中帧内预测模式的代价,比较后得到最佳的帧内预测模式。该函数的等流程大致如下:
(1)进行 Intra16X16
模式的预测
predict_16x16_mode_available()
根据周围宏块的情况判断其可用的预测模式(主要检查左边和上边的块是否可用)。Intra16x16
帧内预测模式:predict_16x16[]()
汇编函数进行 Intra16x16
帧内预测x264_pixel_function_t
中的 mbcmp[]()
计算编码代价(mbcmp[]()
指向 SAD 或者 SATD 汇编函数)。Intra16x16
模式。(2)进行 Intra8x8
模式的预测(未研究,流程应该类似)
(3)进行 Intra4X4
块模式的预测
4x4
的块:x264_mb_predict_intra4x4_mode()
根据周围宏块情况判断该块可用的预测模式。Intra4x4
的帧内预测模式:predict_4x4 []()
汇编函数进行 Intra4x4
帧内预测x264_pixel_function_t
中的 mbcmp[]()
计算编码代价(mbcmp[]()
指向 SAD 或者 SATD 汇编函数)。Intra4x4
模式。4X4
块的最小代价相加,得到总代价。(4)将上述 3 中模式的代价进行对比,取最小者为当前宏块的帧内预测模式。
x264_macroblock_analyse()
对应着 x264 中的分析模块。分析模块主要完成了下面 2 个方面的功能:
(1)对于帧内宏块,分析帧内预测模式
(2)对于帧间宏块,进行运动估计,分析帧间预测模式
详细功能说明
x264_macroblock_encode()
对应着 x264 中的宏块编码模块。宏块编码模块主要完成了 DCT 变换和量化两个步骤。
从图中可以看出,宏块编码模块的 x264_macroblock_encode()
调用了 x264_macroblock_encode_internal()
,而 x264_macroblock_encode_internal()
完成了如下功能:
1 | x264_macroblock_encode_skip()// 编码Skip类型宏块。 |
x264_macroblock_encode()
用于编码宏块。该函数的定义位于 encoder\macroblock.c
x264_macroblock_encode_internal()
的流程大致如下:
(1)如果是 Skip 类型,调用 x264_macroblock_encode_skip()
编码宏块。
(2)如果是 Intra16x16
类型,调用 x264_mb_encode_i16x16()
编码宏块。
(3)如果是 Intra4x4
类型,循环 16 次调用 x264_mb_encode_i4x4()
编码宏块。
(4)如果是 Inter 类型,则不再调用子函数,而是直接进行编码:
16x16
块调用 x264_dct_function_t
的 sub16x16_dct()
汇编函数,求得编码宏块数据 p_fenc
与重建宏块数据 p_fdec
之间的残差(“sub”),并对残差进行 DCT 变换。8x8
的块,对每个 8x8
块分别调用 x264_quant_function_t
的 quant_4x4x4()
汇编函数进行量化。4x4
的块,对每个 4x4
块分别调用 x264_quant_function_t
的 dequant_4x4()
汇编函数进行反量化(用于重建帧)。8x8
的块,对每个 8x8
块分别调用 x264_dct_function_t
的 add8x8_idct()
汇编函数,对残差进行 DCT 反变换,并将反变换后的数据叠加(“add”)至预测数据上(用于重建帧)。(5) 如果对色度编码,调用 x264_mb_encode_chroma()
。
从 Inter 宏块编码的步骤可以看出,编码就是 “DCT变换+量化” 两步的组合。
简单整理一下 x264_mb_encode_i16x16()
的逻辑,如下所示:
(1)调用 predict_16x16[]()
汇编函数对重建宏块数据 p_fdec
进行帧内预测。
(2)调用 x264_dct_function_t
的 sub16x16_dct()
汇编函数,计算重建宏块数据 p_fdec
与编码宏块数据p_fenc
之间的残差,然后对残差做 DCT 变换。
(3)抽取出来 16 个 4x4DCT
小块的 DC 系数,存储于 dct_dc4x4[]
。
(4)分成 4 个 8x8
的块,对每个 8x8
块分别调用 x264_quant_function_t
的 quant_4x4x4()
汇编函数进行量化。
(5)分成 16 个 4x4
的块,对每个 4x4
块分别调用 x264_quant_function_t
的 dequant_4x4()
汇编函数进行反量化(用于重建帧)。
(6)对于 dct_dc4x4[]
中 16 个小块的 DC 系数作如下处理:
x264_dct_function_t
的 dct4x4dc()
汇编函数进行 Hadamard 变换。x264_quant_function_t
的 quant_4x4_dc()
汇编函数进行 DC 系数的量化。x264_dct_function_t
的 idct4x4dc()
汇编函数进行 Hadamard 反变换。x264_quant_function_t
的 dequant_4x4_dc()
汇编函数进行 DC 系数的反量化。16x16
块对应的位置上。(7)调用 x264_dct_function_t
的 add16x16_idct()
汇编函数,对残差进行 DCT 反变换,并将反变换后的数据叠加(“add”)至预测数据上(用于重建帧)。
可以看出 Intra16x16
编码的过程就是一个 “DCT变换 + 量化 + Hadamard变换” 的流程。其中 “DCT变换 + 量化” 是一个通用的编码步骤,而 “Hadamard变换” 是专属于 Intra16x16
宏块的步骤。
简单整理一下 x264_mb_encode_i4x4()
的逻辑,如下所示:
(1)调用 predict_4x4[]()
汇编函数对重建宏块数据 p_fdec
进行帧内预测。
(2)调用 x264_dct_function_t
的 sub4x4_dct ()
汇编函数,计算重建宏块数据 p_fdec
与编码宏块数据 p_fenc
之间的残差,然后对残差做 DCT 变换。
(3)调用 x264_quant_function_t
的 quant_4x4()
汇编函数进行量化。
(4)调用 x264_quant_function_t
的 dequant_4x4()
汇编函数进行反量化(用于重建帧)。
(5)调用 x264_dct_function_t
的 add4x4_idct()
汇编函数,对残差进行 DCT 反变换,并将反变换后的数据叠加(“add”)至预测数据上(用于重建帧)。
可以看出 Intra4x4
编码的过程就是一个 “DCT变换 + 量化” 的流程。
x264_macroblock_write_cavlc()
对应着x264中的熵编码模块。熵编码模块主要完成了编码数据输出的功能。
从图中可以看出,熵编码模块包含两个函数 x264_macroblock_write_cabac()
和x264_macroblock_write_cavlc()
。如果输出设置为 CABAC 编码,则会调用x264_macroblock_write_cabac()
;如果输出设置为 CAVLC 编码,则会调用 x264_macroblock_write_cavlc()
。本文选择 CAVLC 编码输出函数 x264_macroblock_write_cavlc()
进行分析。该函数调用了如下函数:
1 | x264_cavlc_mb_header_i()// 写入I宏块MB Header数据。包含帧内预测模式等。 |
从源代码可以看出,x264_macroblock_write_cavlc()
的流程大致如下:
(1)根据 Slice 类型的不同,调用不同的函数输出宏块头(MB Header):
P Slice
,调用 x264_cavlc_mb_header_p()
B Slice
,调用 x264_cavlc_mb_header_b()
I Slice
,调用 x264_cavlc_mb_header_i()
(2)调用 x264_cavlc_qp_delta()
输出宏块 QP 值
(3)调用 x264_cavlc_block_residual()
输出 CAVLC 编码的残差数据
本文简单记录一下 FFmpeg 的 libavcodec 中与 libx264 接口部分的源代码。该部分源代码位于 “libavcodec/libx264.c” 中。正是有了这部分代码,使得 FFmpeg 可以调用 libx264 编码 H.264 视频。
从图中可以看出,libx264 对应的 AVCodec 结构体 ff_libx264_encoder
中设定编码器初始化函数是 X264_init()
,编码一帧数据的函数是 X264_frame()
,编码器关闭函数是 X264_close()
。
X264_init()
调用了如下函数:
1 | [libx264 API] x264_param_default()// 设置默认参数。 |
X264_frame()调用了如下函数:
1 | [libx264 API] x264_encoder_encode()// 编码一帧数据。 |
X264_close()
调用了如下函数:
1 | [libx264 API] x264_encoder_close()// 关闭编码器。 |
本文简单记录 FFmpeg 中 libavcodec 的 H.264 解码器(H.264 Decoder)的源代码。这个 H.264 解码器十分重要,可以说 FFmpeg 项目今天可以几乎“垄断”视音频编解码技术,很大一部分贡献就来自于这个 H.264 解码器。这个 H.264 解码器一方面功能强大,性能稳定;另一方面源代码也比较复杂,难以深入研究。本文打算梳理一下这个 H.264 解码器的源代码结构,以方便以后深入学习 H.264 使用。
PS:这部分代码挺复杂的,还有不少地方还比较模糊,还需要慢慢学习……
H.264解码器的函数调用关系图如下所示。
下面解释一下图中关键标记的含义。
FFmpeg和H.264解码器之间作为接口的结构体有2个:
ff_h264_parser
:用于解析 H.264 码流的 AVCodecParser 结构体。ff_h264_decoder
:用于解码 H.264 码流的 AVCodec 结构体。函数在图中以方框的形式表现出来。不同的背景色标志了该函数不同的作用:
箭头线标志了函数的调用关系:
每个函数标识了它所在的文件路径。
下文简单记录几个关键的部分。
FFmpeg和H.264解码器之间作为接口的结构体有2个:ff_h264_parser和ff_h264_decoder。
ff_h264_parser
ff_h264_parser是用于解析H.264码流的AVCodecParser结构体。AVCodecParser中包含了几个重要的函数指针:
在ff_h264_parser结构体中,上述几个函数指针分别指向下面几个实现函数:
ff_h264_decoder
ff_h264_decoder是用于解码H.264码流的AVCodec结构体。AVCodec中包含了几个重要的函数指针:
在ff_h264_decoder结构体中,上述几个函数指针分别指向下面几个实现函数:
ff_h264_decode_init():初始化H.264解码器。
h264_decode_frame():解码H.264码流。
h264_decode_end():关闭H.264解码器。
普通内部函数指的是H.264解码器中还没有进行分类的函数。下面举几个例子。
ff_h264_decoder中ff_h264_decode_init()调用的初始化函数:
ff_h264_decoder中h264_decode_frame()逐层调用的和解码Slice相关的函数:
ff_h264_decoder中h264_decode_end()调用的清理函数:
ff_h264_parser中h264_parse()逐层调用的和解析Slice相关的函数:
h264_find_frame_end():查找NALU的结尾。
parse_nal_units():解析一个NALU。
解析函数(Parser)用于解析H.264码流中的一些信息(例如SPS、PPS、Slice Header等)。在parse_nal_units()和decode_nal_units()中都调用这些解析函数完成了解析。下面举几个解析函数的例子。
熵解码函数(Entropy Decoding)读取码流数据并且进行CABAC或者CAVLC熵解码。CABAC解码函数是ff_h264_decode_mb_cabac(),CAVLC解码函数是ff_h264_decode_mb_cavlc()。熵解码函数中包含了很多的读取指数哥伦布编码数据的函数,例如get_ue_golomb_long(),get_ue_golomb(),get_se_golomb(),get_ue_golomb_31()等等。
在获取残差数据的时候需要进行CAVLC/CABAC解码。例如解码CAVLC的时候,会调用decode_residual()函数,而decode_residual()会调用get_vlc2()函数,get_vlc2()会调用OPEN_READER(),UPDATE_CACHE(),GET_VLC(),CLOSE_READER()几个函数读取CAVLC格式的数据。
此外,在获取运动矢量的时候,会调用pred_motion()以及类似的几个函数获取运动矢量相关的信息。
解码函数(Decode)通过帧内预测、帧间预测、DCT反变换等方法解码压缩数据。解码函数是ff_h264_hl_decode_mb()
。其中跟宏块类型的不同,会调用几个不同的函数,最常见的就是调用hl_decode_mb_simple_8()
。
hl_decode_mb_simple_8()
的定义是无法在源代码中直接找到的,这是因为它实际代码的函数名称是使用宏的方式写的(以后再具体分析)。hl_decode_mb_simple_8()
的源代码实际上就是 FUNC(hl_decode_mb)()
函数的源代码。
FUNC(hl_decode_mb)()
根据宏块类型的不同作不同的处理:如果宏块类型是INTRA,就会调用hl_decode_mb_predict_luma()
进行帧内预测;如果宏块类型不是INTRA,就会调用FUNC(hl_motion_422)()
或者 FUNC(hl_motion_420)()
进行四分之一像素运动补偿。
随后 FUNC(hl_decode_mb)()
会调用 hl_decode_mb_idct_luma()
等几个函数对数据进行DCT反变换工作。
环路滤波函数(Loop Filter)对解码后的数据进行滤波,去除方块效应。环路滤波函数是loop_filter()。其中调用了ff_h264_filter_mb()和ff_h264_filter_mb_fast()。ff_h264_filter_mb_fast()中又调用了h264_filter_mb_fast_internal()。而h264_filter_mb_fast_internal()中又调用了下面几个函数进行滤波:
filter_mb_edgeh():亮度水平滤波
filter_mb_edgev():亮度垂直滤波
filter_mb_edgech():色度水平滤波
filter_mb_edgecv():色度垂直滤波
汇编函数(Assembly)是做过汇编优化的函数。为了提高效率,整个H.264解码器中(主要在解码部分和环路滤波部分)包含了大量的汇编函数。实际解码的过程中,FFmpeg会根据系统的特性调用相应的汇编函数(而不是C语言函数)以提高解码的效率。如果系统不支持汇编优化的话,FFmpeg才会调用C语言版本的函数。例如在帧内预测的时候,对于16x16亮度DC模式,有以下几个版本的函数:
在网上找到一张图(出处不详),分析了FFmpeg的H.264解码器每个函数运行的耗时情况,比较有参考意义,在这里附上。
从图中可以看出,熵解码、宏块解码、环路滤波耗时比例分别为:23.64%、51.85%、22.22%。
本文继续分析FFmpeg中libavcodec的H.264解码器(H.264 Decoder)。上篇文章概述了FFmpeg中H.264解码器的结构;从这篇文章开始,具体研究H.264解码器的源代码。本文分析H.264解码器中解析器(Parser)部分的源代码。这部分的代码用于分割H.264的NALU,并且解析SPS、PPS、SEI等信息。解析H.264码流(对应AVCodecParser结构体中的函数)和解码H.264码流(对应AVCodec结构体中的函数)的时候都会调用该部分的代码完成相应的功能。
从图中可以看出,H.264的解析器(Parser)在解析数据的时候调用 h264_parse()
,h264_parse()
调用了parse_nal_units()
,parse_nal_units()
则调用了一系列解析特定 NALU 的函数。H.264 的解码器(Decoder)在解码数据的时候调用 h264_decode_frame()
,h264_decode_frame()
调用了decode_nal_units()
,decode_nal_units()
也同样调用了一系列解析不同 NALU 的函数。
图中简单列举了几个解析特定 NALU 的函数:
1 | ff_h264_decode_nal()// 解析 NALU Header |
H.264 解码器与 H.264 解析器最主要的不同的地方在于它调用了 ff_h264_execute_decode_slices()
函数进行了解码工作。这篇文章只分析 H.264 解析器的源代码,至于 H.264 解码器的源代码,则在后面几篇文章中再进行分析。
h264_find_frame_end()
用于查找 H.264 码流中的 “起始码”(start code)。在 H.264 码流中有两种起始码: 0x000001
和 0x00000001
。其中 4Byte 的长度的起始码最为常见。只有当一个完整的帧被编为多个 slice 的时候,包含这些 slice 的 NALU 才会使用 3Byte 的起始码。h264_find_frame_end()
的定义位于libavcodec\h264_parser.c
从源代码可以看出,h264_find_frame_end()
使用了一种类似于状态机的方式查找起始码。函数中的 for()
循环每执行一遍,状态机的状态就会改变一次。该状态机主要包含以下几种状态:
1 | 7 - 初始化状态 |
这些状态之间的状态转移图如下所示。图中粉红色代表初始状态,绿色代表找到“起始码”的状态。
如图所示,h264_find_frame_end()
初始化时候位于状态 “7”;当找到 1 个 “0” 之后,状态从 “7” 变为 “2”;在状态 “2” 下,如果再次找到 1 个 “0”,则状态变为 “1”;在状态 “1” 下,如果找到 “1”,则状态变换为 “4”,表明找到了 “0x000001” 起始码;在状态 “1” 下,如果找到 “0”,则状态变换为 “0”;在状态 “0” 下,如果找到 “1”,则状态变换为 “5” ,表明找到了 “0x000001” 起始码。
parse_nal_units()
主要做了以下几步处理:
(1)对于所有的 NALU,都调用 ff_h264_decode_nal
解析 NALU 的 Header,得到 nal_unit_type 等信息
(2)根据 nal_unit_type 的不同,调用不同的解析函数进行处理。例如:
a)解析 SPS 的时候调用 ff_h264_decode_seq_parameter_set()
b)解析 PPS 的时候调用 ff_h264_decode_picture_parameter_set()
c)解析 SEI 的时候调用 ff_h264_decode_sei()
d)解析 IDR Slice / Slice 的时候,获取 slice_type 等一些信息。
本文分析FFmpeg的H.264解码器的主干部分。“主干部分” 是相对于 “熵解码”、“宏块解码”、“环路滤波” 这些细节部分而言的。它包含了 H.264 解码器直到 decode_slice()
前面的函数调用关系(decode_slice()
后面就是H.264解码器的细节部分,主要包含了 “熵解码”、“宏块解码”、“环路滤波” 3个部分)。
从图中可以看出,H.264解码器(Decoder)在初始化的时候调用了 ff_h264_decode_init()
,ff_h264_decode_init()
又调用了下面几个函数进行解码器汇编函数的初始化工作(仅举了几个例子):
1 | ff_h264dsp_init()// 初始化DSP相关的汇编函数。包含了IDCT、环路滤波函数等。 |
H.264 解码器在关闭的时候调用了 h264_decode_end()
,h264_decode_end()
又调用了ff_h264_remove_all_refs()
,ff_h264_free_context()
等几个函数进行清理工作。
H.264 解码器在解码图像帧的时候调用了 h264_decode_frame()
,h264_decode_frame()
调用了 decode_nal_units()
,decode_nal_units()
调用了两类函数——解析函数和解码函数,如下所示。
(1)解析函数(获取信息):
1 | ff_h264_decode_nal()// 解析NALU Header。 |
(2)解码函数(解码获得图像):
1 | ff_h264_execute_decode_slices() // 解码Slice。 |
其中 ff_h264_execute_decode_slices()
调用了 decode_slice()
,而 decode_slice()
中调用了解码器中细节处理的函数(暂不详细分析):
1 | ff_h264_decode_mb_cabac()// CABAC熵解码函数。 |
h264_decode_frame()
根据输入的 AVPacket 的 data 是否为空作不同的处理:
(1)若果输入的 AVPacket 的 data 为空,则调用 output_frame()
输出 delayed_pic[]
数组中的H264Picture,即输出解码器中缓存的帧(对应的是通常称为 “Flush Decoder” 的功能)。
(2)若果输入的 AVPacket 的 data 不为空,则首先调用 decode_nal_units()
解码 AVPacket 的 data,然后再调用 output_frame()
输出解码后的视频帧(有一点需要注意:由于帧重排等因素,输出的 AVFrame 并非对应于输入的 AVPacket)。
decode_nal_units()
首先调用 ff_h264_decode_nal()
判断 NALU 的类型,然后根据 NALU 类型的不同调用了不同的处理函数。这些处理函数可以分为两类——解析函数和解码函数,如下所示。
(1)解析函数(获取信息):
1 | ff_h264_decode_seq_parameter_set()// 解析SPS。 |
(2)解码函数(解码得到图像):
1 | ff_h264_execute_decode_slices()// 解码Slice。 |
decode_slice()
按照宏块(16x16
)的方式处理输入的视频流。每个宏块的压缩数据经过以下 3 个基本步骤的处理,得到解码后的数据:
(1)熵解码。如果熵编码为 CABAC,则调用 ff_h264_decode_mb_cabac()
;如果熵编码为 CAVLC,则调用 ff_h264_decode_mb_cavlc()
(2)宏块解码。这一步骤调用 ff_h264_hl_decode_mb()
(3)环路滤波。这一步骤调用 loop_filter()
此外,还有可能调用错误隐藏函数 er_add_slice()
。
至此,decode_nal_units()
函数的调用流程就基本分析完毕了。h264_decode_frame()
在调用完 decode_nal_units()
之后,还需要把解码后得到的 H264Picture 转换为 AVFrame 输出出来,这时候会调用一个相对比较简单的函数 output_frame()
。
FFmpeg的H.264解码器调用 decode_slice()
函数完成了解码工作。这些解码工作可以大体上分为3个步骤:熵解码,宏块解码以及环路滤波。本文分析这3个步骤中的第1个步骤。
从图中可以看出,FFmpeg的熵解码方面的函数有两个:ff_h264_decode_mb_cabac()
和 ff_h264_decode_mb_cavlc()
。
ff_h264_decode_mb_cabac()
用于解码 CABAC 编码方式的 H.264 数据,ff_h264_decode_mb_cavlc()
用于解码 CAVLC 编码方式的 H.264 数据。本文挑选了ff_h264_decode_mb_cavlc()
函数进行分析。
ff_h264_decode_mb_cavlc()
调用了很多的读取指数哥伦布编码数据的函数,例如 get_ue_golomb_long()
,get_ue_golomb(),get_se_golomb()
,get_ue_golomb_31()
等。此外在解码残差数据的时候,调用了 decode_residual()
函数,而 decode_residual()
会调用 get_vlc2()
函数读取 CAVLC 编码数据。
总而言之,“熵解码” 部分的作用就是按照 H.264 语法和语义的规定,读取数据(宏块类型、运动矢量、参考帧、残差等)并且赋值到 FFmpeg H.264 解码器中相应的变量上。需要注意的是,“熵解码” 部分并不使用这些变量还原视频数据。还原视频数据的功能在下一步 “宏块解码” 步骤中完成。
在开始看 ff_h264_decode_mb_cavlc()
之前先回顾一下 decode_slice()
函数。
decode_slice()
的的流程如下:
(1)判断 H.264 码流是 CABAC 编码还是 CAVLC 编码,进入不同的处理循环。
(2)如果是 CABAC 编码,首先调用 ff_init_cabac_decoder()
初始化 CABAC 解码器。然后进入一个循环,依次对每个宏块进行以下处理:
a)调用 ff_h264_decode_mb_cabac()
进行 CABAC 熵解码
b)调用 ff_h264_hl_decode_mb()
进行宏块解码
c)解码一行宏块之后调用 loop_filter()
进行环路滤波
d)此外还有可能调用 er_add_slice()
进行错误隐藏处理
(3)如果是 CABAC 编码,直接进入一个循环,依次对每个宏块进行以下处理:
a)调用 ff_h264_decode_mb_cavlc()
进行 CAVLC 熵解码
b)调用 ff_h264_hl_decode_mb()
进行宏块解码
c)解码一行宏块之后调用 loop_filter()
进行环路滤波
d)此外还有可能调用 er_add_slice()
进行错误隐藏处理
可以看出,出了熵解码以外,宏块解码和环路滤波的函数是一样的。
ff_h264_decode_mb_cavlc()
的定义有将近 1000 行代码,算是一个比较复杂的函数了。我在其中写了不少注释,因此不再对源代码进行详细的分析。下面先简单梳理一下它的流程:
(1)解析 Skip 类型宏块
(2)获取 mb_type
(3)填充当前宏块左边和上边宏块的信息(后面的预测中会用到)
(4)根据 mb_type
的不同,分成三种情况进行预测工作:
a)宏块是帧内预测
Intra4x4
类型,则需要单独解析帧内预测模式。Intra16x16
类型,则不再做过多处理。b)宏块划分为 4 个块(此时每个 8x8
的块可以再次划分为 4 种类型)
这个时候每个 8x8
的块可以再次划分为 8x8、8x4、4x8、4x4
几种子块。需要分别处理这些小的子块:
c)其它类型(包括 16x16,16x8,8x16
几种划分,这些划分不可再次划分)
这个时候需要判断宏块的类型为 16x16,16x8
还是 8x16
,然后作如下处理:
(5)解码残差信息
(6)将宏块的各种信息输出到整个图片相应的变量中
在 H.264 解码器中包含了各种各样的 Cache(缓存)。例如:
1 | intra4x4_pred_mode_cache// Intra4x4帧内预测模式的缓存 |
其他知识查看
FFmpeg的H.264解码器调用 decode_slice()
函数完成了解码工作。这些解码工作可以大体上分为3个步骤:熵解码,宏块解码以及环路滤波。本文分析这3个步骤中的第2个步骤。由于宏块解码部分的内容比较多,因此将本部分内容拆分成两篇文章:一篇文章记录帧内预测宏块(Intra)的宏块解码,另一篇文章记录帧间预测宏块(Inter)的宏块解码。
宏块解码函数(Decode)通过帧内预测、帧间预测、DCT 反变换等方法解码压缩数据。解码函数是 ff_h264_hl_decode_mb()
。其中跟宏块类型的不同,会调用几个不同的函数,最常见的就是调用 hl_decode_mb_simple_8()
。
hl_decode_mb_simple_8()
的定义是无法在源代码中直接找到的,这是因为它实际代码的函数名称是使用宏的方式写的。hl_decode_mb_simple_8()
的源代码实际上就是 FUNC(hl_decode_mb)()
函数的源代码。
从函数调用图中可以看出,FUNC(hl_decode_mb)()
根据宏块类型的不同作不同的处理:
hl_decode_mb_predict_luma()
进行帧内预测;FUNC(hl_motion_422)()
或者 FUNC(hl_motion_420)()
进行四分之一像素运动补偿。经过帧内预测或者帧间预测步骤之后,就得到了预测数据。随后 FUNC(hl_decode_mb)()
会调用 hl_decode_mb_idct_luma()
等几个函数对残差数据进行 DCT 反变换工作,并将变换后的数据叠加到预测数据上,形成解码后的图像数据。
由于帧内预测宏块和帧间预测宏块的解码工作都比较复杂,因此分成两篇文章记录这两部分的源代码。本文记录帧内预测宏块解码时候的源代码。
下面简单梳理一下 FUNC(hl_decode_mb)
的流程(在这里只考虑亮度分量的解码,色度分量的解码过程是类似的):
(1)预测
hl_decode_mb_predict_luma()
进行帧内预测,得到预测数据。FUNC(hl_motion_420)()
或者 FUNC(hl_motion_422)()
进行帧间预测(即运动补偿),得到预测数据。(2)残差叠加
hl_decode_mb_idct_luma()
对 DCT 残差数据进行 DCT 反变换,获得残差像素数据并且叠加到之前得到的预测数据上,得到最后的图像数据。PS:该流程中有一个重要的贯穿始终的内存指针 dest_y
,其指向的内存中存储了解码后的亮度数据。
根据原代码梳理一下 hl_decode_mb_predict_luma()
的主干:
(1)如果宏块是4x4帧内预测类型(Intra4x4),作如下处理:
4x4
的块,并作如下处理:intra4x4_pred_mode_cache
中读取 4x4
帧内预测方法pred4x4()
进行帧内预测h264_idct_add()
对 DCT 残差数据进行 4x4DCT
反变换;如果DCT 系数中不包含 AC 系数的话,则调用汇编函数 h264_idct_dc_add()
对残差数据进行 4x4DCT
反变换(速度更快)。(2)如果宏块是 16x16
帧内预测类型(Intra4x4
),作如下处理:
intra16x16_pred_mode
获得 16x16
帧内预测方法pred16x16 ()
进行帧内预测h264_luma_dc_dequant_idct ()
对 16 个小块的 DC 系数进行Hadamard 反变换在这里需要注意,帧内 4x4
的宏块在执行完 hl_decode_mb_predict_luma()
之后实际上已经完成了 “帧内预测+DCT反变换” 的流程(解码完成);而帧内 16x16
的宏块在执行完 hl_decode_mb_predict_luma()
之后仅仅完成了 “帧内预测+Hadamard反变换 ”的流程,而并未进行 “DCT反变换” 的步骤,这一步骤需要在后续步骤中完成。
下文记录上述流程中涉及到的汇编函数(此处暂不记录DCT反变换的函数,在后文中再进行叙述):
4x4
帧内预测汇编函数:H264PredContext -> pred4x4[dir
]()
16x16
帧内预测汇编函数:H264PredContext -> pred16x16[dir]()
Hadamard反变换汇编函数:H264DSPContext->h264_luma_dc_dequant_idct()
下面根据源代码简单梳理一下 hl_decode_mb_idct_luma()
的流程:
(1)判断宏块是否属于 Intra4x4
类型,如果是,函数直接返回(Intra4x4
比较特殊,它的 DCT 反变换已经前文所述的 “帧内预测” 部分完成)。
(2)根据不同的宏块类型作不同的处理:
Intra16x16
:调用 H264DSPContext 的汇编函数 h264_idct_add16intra()
进行 DCT 反变换h264_idct_add16()
进行 DCT 反变换PS:需要注意的是 h264_idct_add16intra()
和 h264_idct_add16()
只有微小的区别,它们的基本逻辑都是把 16x16
的块划分为 16 个 4x4
的块再进行 DCT 反变换。此外还有一点需要注意:函数名中的 “add” 的含义是将 DCT 反变换之后的残差像素数据直接叠加到已有数据之上。
本文分析FFmpeg的H.264解码器的宏块解码(Decode)部分。FFmpeg的H.264解码器调用 decode_slice()
函数完成了解码工作。这些解码工作可以大体上分为3个步骤:熵解码,宏块解码以及环路滤波。本文分析这3个步骤中的第2个步骤:宏块解码。上一篇文章已经记录了帧内预测宏块(Intra)的宏块解码,本文继续上一篇文章的内容,记录帧间预测宏块(Inter)的宏块解码。
参考宏块解码(Decode)部分的源代码的调用关系图
MCFUNC(hl_motion)
根据子宏块的划分类型的不同,传递不同的参数调用 mc_part()
函数。
(1)如果子宏块划分为 16x16
(等同于没有划分),直接调用 mc_part()
并且传递如下参数:
qpix_put[0]
(qpix_put[0]
中的函数进行 16x16
块的四分之一像素运动补偿)。qpix_avg[0]
。(2)如果子宏块划分为 16x8
,分两次调用 mc_part()
并且传递如下参数:
qpix_put[1]
(qpix_put[1]
中的函数进行 8x8
块的四分之一像素运动补偿)。qpix_avg[1]
。其中第 1 次调用 mc_part()
的时候 x_offset 和 y_offset 都设置为 0,第 2 次调用 mc_part()
的时候 x_offset 设置为 0,y_offset 设置为 4。
(3)如果子宏块划分为 8x16
,分两次调用 mc_part()
并且传递如下参数:
qpix_put[1]
(qpix_put[1]
中的函数进行 8x8
块的四分之一像素运动补偿)。qpix_avg[1]
。8 * h->mb_linesize
。其中第 1 次调用 mc_part()
的时候 x_offset 和 y_offset 都设置为 0,第 2 次调用 mc_part()
的时候 x_offset 设置为 4,y_offset 设置为 0。
(4)如果子宏块划分为 8x8
,说明此时每个 8x8
子宏块还可以继续划分为 8x8,8x8,4x8,4x4
几种类型,此时根据上述的规则,分成 4 次分别对这些小块做类似的处理。
qpix_put[4][16]
实际上指向了 H264QpelContex 的 put_h264_qpel_pixels_tab[4][16]
,其中存储了所有单向预测方块的四分之一像素运动补偿函数。其中:
1 | qpix_put[0]存储的是16x16方块的运动补偿函数; |
从源代码可以看出,mc_part_std()
首先计算了几个关键的用于确定子宏块位置的参数,然后根据预测类型的不同(单向预测或者双向预测),把不同的函数指针传递给 mc_dir_part()
:如果仅仅使用了 list0(单向预测),则只传递 qpix_put()
;如果使用了 list0 和 list1(双向预测),则调用两次 mc_dir_part()
,第一次传递 qpix_put()
,第二次传递 qpix_avg()
。
mc_part_std()
中赋值了 3 个重要的变量(只考虑亮度):
(1)dest_y
:指向子宏块亮度数据指针。这个值是通过 x_offset 和 y_offset 计算得来的。在这里需要注意一点:x_offset 和 y_offset 是以色度为基本单位的,所以在计算亮度相关的变量的时候需要乘以 2。
(2)x_offset
:传入的 x_offset 本来是子宏块相对于整个宏块位置的横坐标,在这里加上 8 * h->mb_x
之后,变成了子宏块相对于整个图像的位置的横坐标(以色度为基本单位)。
(3)y_offset
:传入的 y_offset 本来是子宏块相对于整个宏块位置的纵坐标,在这里加上 8 * h->mb_y
之后,变成了子宏块相对于整个图像的位置的纵坐标(以色度为基本单位)。
通过源代码,简单梳理一下 mc_dir_part()
的流程(只考虑亮度,色度的流程类似):
(1)计算 mx 和 my。mx 和 my 是当前宏块的匹配块的位置坐标。需要注意的是该坐标是以 1/4
像素(而不是整像素)为基本单位的。
(2)计算 offset。offset 是当前宏块的匹配块相对于图像的整像素偏移量,由 mx、my 计算而来。
(3)计算 luma_xy。luma_xy 决定了当前宏块的匹配块采用的四分之一像素运动补偿的方式,由 mx、my 计算而来。
(4)调用运动补偿汇编函数 qpix_op[luma_xy]()
完成运动补偿。在这里需要注意,如果子宏块不是正方形的(square 取 0),则还会调用 1 次 qpix_op[luma_xy]()
完成另外一个方块的运动补偿。
总而言之,首先找到当前宏块的匹配块的整像素位置,然后在该位置的基础上进行四分之一像素的内插,并将结果输出出来。
前文中曾经提过,由于 H.264 解码器中只提供了正方形块的四分之一像素运动补偿函数,所以如果子宏块不是正方形的(例如 16x8,8x16
),就需要先将子宏块划分为正方形的方块,然后再进行两次运动补偿(两个正方形方块之间的位置关系用 delta 变量记录)。例如 16x8
的宏块,就会划分成两个 8x8
的方块,调用两次相同的运动补偿函数
下面可以看一下 C 语言版本的四分之一像素运动补偿函数的源代码。由于 1/4
像素内插比较复杂,其中还用到了整像素赋值函数以及 1/2
像素线性内插函数,所以需要从简到难一步一步的看这些源代码。打算按照顺序一步一步分析这些源代码:
(1)pel_template.c(展开“ DEF_PEL(put, op_put)
”宏):整像素赋值(用于整像素的单向预测)
(2)pel_template.c(展开“ DEF_PEL(avg, op_avg)
”宏):整像素求平均(写这个为了举一个双向预测的例子)
(3)hpel_template.c((展开“DEF_HPEL(put, op_put)
”宏):1/2
像素线性内插
(4)h264qpel_template.c(展开“ H264_LOWPASS(put_, op_put, op2_put)
”宏):半像素内插(注意不是1/2像素线性内插,而是需要滤波的)
(5)h264qpel_template.c(展开“H264_MC(put_, 8)
”宏):1/4
像素运动补偿
本文分析FFmpeg的H.264解码器的环路滤波(Loop Filter)部分。FFmpeg的H.264解码器调用decode_slice()函数完成了解码工作。这些解码工作可以大体上分为3个步骤:熵解码,宏块解码以及环路滤波。本文分析这3个步骤中的第3个步骤。
环路滤波主要用于滤除方块效应。decode_slice()
在解码完一行宏块之后,会调用 loop_filter()
函数完成环路滤波功能。loop_filter()
函数会遍历该行宏块中的每一个宏块,并且针对每一个宏块调用 ff_h264_filter_mb_fast()
。ff_h264_filter_mb_fast()
又会调用 h264_filter_mb_fast_internal()
。
h264_filter_mb_fast_internal()
完成了一个宏块的环路滤波工作。该函数调用 filter_mb_edgev()
和 filter_mb_edgeh()
对亮度垂直边界和水平边界进行滤波,或者调用 filter_mb_edgecv()
和 filter_mb_edgech()
对色度的的垂直边界和水平边界进行滤波。
通过源代码整理出来 h264_filter_mb_fast_internal()
的流程如下:
(1)读取 QP 等几个参数,用于推导滤波门限值 alpha,beta。
(2)如果是帧内宏块(Intra),作如下处理:
a)对于水平的边界,调用 filter_mb_edgeh()
进行滤波。
b)对于垂直的边界,调用 filter_mb_edgev()
进行滤波。
帧内宏块滤波过程中,对于在宏块边界上的边界(最左边的垂直边界和最上边的水平边界),采用滤波强度 Bs 为 4 的滤波;对于其它边界则采用滤波强度 Bs 为 3 的滤波。
(3)如果是其他宏块,作如下处理:
a)对于水平的边界,调用 filter_mb_edgeh()
进行滤波。
b)对于垂直的边界,调用 filter_mb_edgev()
进行滤波。
此类宏块的滤波强度需要另作判断。
总体说来,一个宏块内部的滤波顺序如下图所示。图中的 “0”、“1”、“2”、“3” 为滤波的顺序。可以看出首先对垂直边界进行滤波,然后对水平边界进行滤波。垂直边界滤波按照从左到右的顺序进行,而水平边界的滤波按照从上到下的顺序进行。
NAL 全称 Network Abstract Layer,即网络抽象层。在 H.264/AVC 视频编码标准中,整个系统框架被分为了两个层面:视频编码层面(VCL)和网络抽象层面(NAL)。其中,前者负责有效表示视频数据的内容,而后者则负责格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。
现实中的传输系统是多样化的,其可靠性,服务质量,封装方式等特征各不相同,NAL 这一概念的提出提供了一个视频编码器和传输系统的友好接口,使得编码后的视频数据能够有效地在各种不同的网络环境中传输。
NAL 单元是 NAL 的基本语法结构,它包含一个字节的头信息和一系列来自 VCL 的称为原始字节序列载荷
(RBSP)的字节流。头信息中包含着一个可否丢弃的指示标记,标识着该 NAL 单元的丢弃能否引起错误扩散,一般,如果 NAL 单元中的信息不用于构建参考图像,则认为可以将其丢弃;最后包含的是NAL 单元的类型信息,暗示着其内含有效载荷的内容。 送到解码器端的 NAL 单元必须遵守严格的顺序,如果应用程序接收到的 NAL 单元处于乱序,则必须提供一种恢复其正确顺序的方法。
NAL 提供了一个编解码器与传输网络的通用接口,而对于不同的网络环境,具体的实现方案是不同的。对于基于流的传输系统如 H.320、MPEG 等,需要按照解码顺序组织 NAL 单元,并为每个 NAL 单元增加若干比特字节对齐的前缀以形成字节流;对于 RTP/UDP/IP 系统,则可以直接将编码器输出的 NAL 单元作为 RTP 的有效载荷;而对于同时提供多个逻辑信道的传输系统,甚至可以根据重要性将不同类型的NAL 单元在不同服务质量的信道中传输。
为了实现编解码器良好的网络适应性,需要做两方面的工作:
第一、在 Codec 中将 NAL 这一技术完整而有效的实现;
第二、在遵循 H.264/AVC NAL 规范的前提下设计针对不同网络的最佳传输方案。
如果实现了以上两个目标,所实现的就不仅仅是一种视频编解码技术,而是一套适用范围很广的多媒体传输方案,该方案适用于如视频会议,数据存储,电视广播,流媒体,无线通信,远程监控等多种领域。
标识 NAL 单元中的 RBSP 数据类型,其中,nal_unit_type 为 1, 2, 3, 4, 5 的 NAL 单元称为 VCL 的 NAL单元,其他类型的 NAL 单元为非 VCL 的 NAL 单元。
TODO
TODO
TODO
NAL 的头占用了一个字节,按照比特自高至低排列可以表示如下:
1 | 0AABBBBB |
其中,AA 用于表示该 NAL 是否可以丢弃(有无被其后的 NAL 参考),00b 表示没有参考作用,可丢弃,如 B slice、SEI 等,非零——包括 01b、10b、11b——表示该 NAL 不可丢弃,如 SPS、PPS、I Slice、P Slice 等。
常用的 NAL 头的取值如:
1 | 0x67: SPS |
由于 NAL 的语法中没有给出长度信息,实际的传输、存储系统需要增加额外的头实现各个 NAL 单元的定界。其中,AVI 文件和 MPEG TS 广播流采取的是字节流的语法格式,即在 NAL 单元之前增加 0x00000001 的同步码,则从 AVI 文件或 MPEG TS PES 包中读出的一个 H.264 视频帧以下面的形式存在:
1 | 00 00 00 01 06 ... 00 00 00 01 67 ... 00 00 00 01 68 ... 00 00 00 01 65 ... |
而对于 MP4 文件,NAL 单元之前没有同步码,却有若干字节的长度码,来表示 NAL 单元的长度,这个长度码所占用的字节数由 MP4 文件头给出;此外,从 MP4 读出来的视频帧不包含 PPS 和 SPS,这些信息位于 MP4的文件头中,解析器必须在打开文件的时候就获取它们。从 MP4 文件读出的一个 H.264 帧往往是下面的形式(假设长度码为 2 字节):
1 | 00 19 06 [... 25 字节...] 24 aa 65 [... 9386 字节...] |
[总结]FFMPEG视音频编解码零基础学习方法
最简单的基于FFMPEG+SDL的视频播放器 ver2 (采用SDL2.0)
FFmpeg 解码一个视频流程:
SDL2.0 显示 YUV 的流程:
最简单的基于FFMPEG的视频编码器(YUV编码为H.264)
最简单的基于FFmpeg的视频编码器-更新版(YUV编码为HEVC(H.265))
最简单的基于FFmpeg的编码器-纯净版(不包含libavformat)
通过该流程,不仅可以编码H.264/H.265的码流,而且可以编码MPEG4/MPEG2/VP9/VP8等多种码流。实际上使用FFmpeg编码视频的方式都是一样的。图中蓝色背景的函数是实际输出数据的函数。浅绿色的函数是视频编码的函数。
简单介绍一下流程中各个函数的意义:
1 | av_register_all() // 注册FFmpeg所有编解码器。 |
以下记录一个更加 “纯净” 的基于 FFmpeg 的视频编码器。此前记录过一个基于 FFmpeg 的视频编码器:
《最简单的基于FFmpeg的视频编码器-更新版(YUV编码为HEVC(H.265))》
这个视频编码器调用了 FFmpeg 中的 libavformat 和 libavcodec 两个库完成了视频编码工作。但是这不是一个 “纯净” 的编码器。
上述两个库中 libavformat 完成封装格式处理,而 libavcodec 完成编码工作。
一个 “纯净” 的编码器,理论上说只需要使用 libavcodec 就足够了,并不需要使用 libavformat。一下记录的编码器就是这样的一个 “纯净” 的编码器,它仅仅通过调用 libavcodec 将 YUV 数据编码为 H.264/HEVC 等格式的压缩视频码流。
仅使用libavcodec(不使用libavformat)编码视频的流程:
流程图中关键函数的作用如下所列:
1 | avcodec_register_all() // 注册所有的编解码器。 |
两个存储数据的结构体如下所列:
1 | AVFrame // 存储一帧未编码的像素数据。 |
对比:
简单记录一下这个只使用 libavcodec 的 “纯净版” 视频编码器和使用 libavcodec+libavformat 的视频编码器的不同。
(1) 下列与libavformat相关的函数在“纯净版”视频编码器中都不存在。
1 | av_register_all注册所有的编解码器,复用/解复用器等等组件。其中调用了 |
(2) 新增了如下几个函数
1 | avcodec_register_all() // 只注册编解码器有关的组件。 |
可以看出,相比于“完整”的编码器,这个纯净的编码器函数调用更加简单,功能相对少一些,相对来说更加的“轻量”。
函数解析
ffmpeg 注册复用器,编码器等的函数 av_register_all()
。该函数在所有基于ffmpeg的应用程序中几乎都是第一个被调用的。只有调用了该函数,才能使用复用器,编码器等。
函数调用关系图如下图所示。av_register_all()
调用了 avcodec_register_all()
。avcodec_register_all()
注册了和编解码器有关的组件:硬件加速器,解码器,编码器,Parser,Bitstream Filter。av_register_all()
除了调用 avcodec_register_all()
之外,还注册了复用器,解复用器,协议处理器。
内存操作的常见函数位于 libavutil\mem.c
中。本文记录FFmpeg开发中最常使用的几个函数:av_malloc()
,av_realloc()
,av_mallocz()
,av_calloc()
,av_free()
,av_freep()
。
av_malloc()
就是简单的封装了系统函数malloc(),并做了一些错误检查工作。
size _t 这个类型在 FFmpeg 中多次出现,简单解释一下其作用。size _t 是为了增强程序的可移植性而定义的。不同系统上,定义 size_t 可能不一样。它实际上就是 unsigned int。
FFmpeg 内存分配方面多次涉及到 “内存对齐”(memory alignment)的概念。
这方面内容在 IBM 的网站上有一篇文章,讲的挺通俗易懂的,在此简单转述一下。
程序员通常认为内存就是一个字节数组,每次可以一个一个字节存取内存。例如在 C 语言中使用 char *
指代 “一块内存”,Java 中使用 byte[]
指代一块内存。如下所示。
但那实际上计算机处理器却不是这样认为的。处理器相对比较 “懒惰”,它会以 2 字节,4 字节,8 字节,16 字节甚至 32 字节来存取内存。例如下图显示了以 4 字节为单位读写内存的处理器 “看待” 上述内存的方式。
上述的存取单位的大小称之为内存存取粒度。
下面看一个实例,分别从地址0,和地址 1 读取 4 个字节到寄存器。
从程序员的角度来看,读取方式如下图所示。
而 2 字节存取粒度的处理器的读取方式如下图所示。
可以看出 2 字节存取粒度的处理器从地址 0 读取 4 个字节一共读取 2 次;从地址 1 读取 4 个字节一共读取了 3 次。由于每次读取的开销是固定的,因此从地址 1 读取 4 字节的效率有所下降。
4 字节存取粒度的处理器的读取方式如下图所示。
可以看出 4 字节存取粒度的处理器从地址 0 读取 4 个字节一共读取 1 次;从地址 1 读取 4 个字节一共读取了 2 次。从地址 1 读取的开销比从地址 0 读取多了一倍。由此可见内存不对齐对 CPU 的性能是有影响的。
1 | av_malloc() // 是FFmpeg中最常见的内存分配函数, av_malloc()就是简单的封装了系统函数malloc() |
FFMPEG中最关键的结构体之间的关系
常见的结构体如下:
1 | // 统领全局的基本结构体。主要用于处理封装格式(FLV/MKV/RMVB 等) |
他们之间的关系如下图所示:
简单分析一下上述几个结构体的初始化和销毁函数。这些函数列表如下。
结构体 | 初始化 | 销毁 |
---|---|---|
AVFormatContext | avformat_alloc_context() | avformat_free_context() |
AVIOContext | avio_alloc_context() | |
AVStream | avformat_new_stream() | |
AVCodecContext | avcodec_alloc_context3() | |
AVFrame | av_frame_alloc(); av_image_fill_arrays() | av_frame_free() |
AVPacket | av_init_packet(); av_new_packet() | av_free_packet() |
avformat_alloc_context()
的定义位于 libavformat\options.c
。
avformat_alloc_context()
调用 av_malloc()
为 AVFormatContext 结构体分配了内存,而且同时也给 AVFormatContext 中的 internal
字段分配内存(这个字段是 FFmpeg 内部使用的,先不分析)。此外调用了一个 avformat_get_context_defaults()
函数。该函数用于设置 AVFormatContext 的字段的默认值。它的定义也位于 libavformat\options.c
,确切的说就位于 avformat_alloc_context()
上面
avformat_get_context_defaults()
首先调用 memset()
将 AVFormatContext 的所有字段置 0。而后调用了一个函数 av_opt_set_defaults()
。av_opt_set_defaults()
用于给字段设置默认值。
avformat_alloc_context()
代码的函数调用关系如下图所示。
avformat_free_context()
的声明位于 libavformat\avformat.h
avformat_free_context()
的定义位于 libavformat\options.c
avformat_free_context()
调用了各式各样的销毁函数:av_opt_free()
,av_freep()
,av_dict_free()
。这些函数分别用于释放不同种类的变量,在这里不再详细讨论。
在这里看一个释放 AVStream 的函数 ff_free_stream()
。该函数的定义位于 libavformat\options.c
(其实就在 avformat_free_context()
上方), 与释放 AVFormatContext 类似,释放 AVStream 的时候,也是调用了 av_freep()
,av_dict_free()
这些函数释放有关的字段。如果使用了 parser 的话,会调用 av_parser_close()
关闭该 parser。
AVIOContext 的初始化函数是 avio_alloc_context()
,销毁的时候使用 av_free()
释放掉其中的缓存即可。它的声明位于 libavformat\avio.h
中
avio_alloc_context()
定义位于 libavformat\aviobuf.c
中
avio_alloc_context()
首先调用 av_mallocz()
为 AVIOContext 分配内存。而后调用了一个函数 ffio_init_context()
。该函数完成了真正的初始化工作
avformat_new_stream()
的声明位于 libavformat\avformat.h
中
AVStream 的初始化函数是 avformat_new_stream()
,销毁函数使用销毁 AVFormatContext 的 avformat_free_context()
就可以了。
avformat_new_stream()
的定义位于 libavformat\utils.c
中
avformat_new_stream()
首先调用 av_mallocz()
为 AVStream 分配内存。接着给新分配的AVStream 的各个字段赋上默认值。然后调用了另一个函数 avcodec_alloc_context3()
初始化 AVStream 中的 AVCodecContext。
avcodec_alloc_context3()
的声明位于 libavcodec\avcodec.h
中
avcodec_alloc_context3()
的定义位于 libavcodec\options.c
中
avcodec_alloc_context3()
首先调用 av_malloc()
为 AVCodecContext 分配存储空间,然后调用了一个函数 avcodec_get_context_defaults3()
用于设置该 AVCodecContext 的默认值
avformat_new_stream()
函数的调用结构如下所示:
AVFrame 的初始化函数是 av_frame_alloc()
,销毁函数是 av_frame_free()
。在这里有一点需要注意,旧版的 FFmpeg 都是使用 avcodec_alloc_frame()
初始化 AVFrame 的,但是我在写这篇文章的时候,avcodec_alloc_frame()
已经被标记为 “过时的” 了,为了保证与时俱进,决定分析新的API——av_frame_alloc()
。
av_frame_alloc()
的声明位于 libavutil\frame.h
av_frame_alloc()
的定义位于 libavutil\frame.c
av_frame_alloc()
首先调用 av_mallocz()
为 AVFrame 结构体分配内存。而后调用了一个函数get_frame_defaults()
用于设置一些默认参数
从 av_frame_alloc()
的代码我们可以看出,该函数并没有为 AVFrame 的像素数据分配空间。因此AVFrame 中的像素数据的空间需要自行分配空间,例如使用 avpicture_fill()
, av_image_fill_arrays()
等函数。
av_frame_alloc()
函数的调用结构如下所示:
avpicture_fill()
的声明位于 libavcodec\avcodec.h
avpicture_fill()
的定义位于 libavcodec\avpicture.c
avpicture_fill()
仅仅是简单调用了一下 av_image_fill_arrays()
。也就是说这两个函数实际上是等同的
av_image_fill_arrays()
的声明位于 libavutil\imgutils.h
中
av_image_fill_arrays()
的定义位于 libavutil\imgutils.c
中
av_image_fill_arrays()
函数中包含 3 个函数:av_image_check_size()
,av_image_fill_linesizes()
,av_image_fill_pointers()
。av_image_check_size()
用于检查输入的宽高参数是否合理,即不能太大或者为负数。av_image_fill_linesizes()
用于填充dst_linesize。av_image_fill_pointers()
则用于填充 dst_data。它们的定义相对比较简单,不再详细分析。
avpicture_fill()
函数调用关系如下图所示:
av_init_packet()
的声明位于 libavcodec\avcodec.h
av_init_packet()
的定义位于 libavcodec\avpacket.c
av_new_packet()
的声明位于 libavcodec\avcodec.h
av_new_packet()
的定义位于 libavcodec\avpacket.c
av_new_packet()
调用了 av_init_packet(pkt)
。此外还调用了一个函数 packet_alloc()
packet_alloc()
中调用 av_buffer_realloc()
为 AVPacket 分配内存。然后调用 memset()
将分配的内存置 0。
PS:发现 AVPacket 的结构随着 FFmpeg 的发展越发复杂了。原先 AVPacket 中的数据仅仅存在一个 uint8_t 类型的数组里,而现在已经使用一个专门的结构体 AVBufferRef 存储数据。
av_new_packet()
代码的函数调用关系如下图所示:
av_free_packet()
的声明位于 libavcodec\avcodec.h
av_free_packet()
的定义位于 libavcodec\avpacket.c
av_free_packet()
调用 av_buffer_unref()
释放 AVPacket 中的数据,而后还调用了av_packet_free_side_data()
释放了 side_data(存储封装格式可以提供的额外的数据)。
该函数用于打开 FFmpeg 的输入输出文件。avio_open2()
的声明位于 libavformat\avio.h
文件中
1 | int avio_open2(AVIOContext **s, const char *url, int flags, |
avio_open2()
函数参数的含义如下:
1 | s:函数调用成功之后创建的AVIOContext结构体。 |
函数调用结构图:
avcodec_find_encoder()
用于查找 FFmpeg 的编码器,
avcodec_find_decoder()
用于查找 FFmpeg 的解码器。
avcodec_find_encoder()
的声明位于 libavcodec\avcodec.h
1 | AVCodec *avcodec_find_encoder(enum AVCodecID id); |
函数的参数是一个编码器的 ID,返回查找到的编码器(没有找到就返回NULL)。
avcodec_find_decoder()
的声明也位于 libavcodec\avcodec.h
1 | AVCodec *avcodec_find_decoder(enum AVCodecID id); |
函数的参数是一个解码器的 ID,返回查找到的解码器(没有找到就返回NULL)。
avcodec_find_encoder()
和 avcodec_find_decoder()
的函数调用关系图如下所示:
avcodec_find_encoder()
的源代码位于 libavcodec\utils.c
avcodec_find_encoder()
调用了一个 find_encdec()
,注意它的第二个参数是 1。
find_encdec()
的源代码位于 libavcodec\utils.c
find_encdec()
中有一个循环,该循环会遍历 AVCodec 结构的链表,逐一比较输入的 ID 和每一个编码器的 ID,直到找到 ID 取值相等的编码器。
在这里有几点需要注意:
(1)first_avcodec 是一个全局变量,存储 AVCodec 链表的第一个元素。
(2)remap_deprecated_codec_id()
用于将一些过时的编码器 ID 映射到新的编码器 ID。
(3)函数的第二个参数 encoder 用于确定查找编码器还是解码器。当该值为 1 的时候,用于查找编码器,此时会调用 av_codec_is_encoder()
判断 AVCodec 是否为编码器;当该值为 0 的时候,用于查找解码器,此时会调用 av_codec_is_decoder()
判断 AVCodec 是否为解码器。
avcodec_find_decoder()
的源代码位于 libavcodec\utils.c
avcodec_find_decoder()
同样调用了 find_encdec()
,只是第 2 个参数设置为 0。
该函数用于初始化一个视音频编解码器的 AVCodecContext。
avcodec_open2()
的声明位于 libavcodec\avcodec.h
1 | int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options); |
用中文简单转述一下avcodec_open2()各个参数的含义:
1 | avctx:需要初始化的 AVCodecContext。 |
avcodec_open2()
函数调用关系非常简单,如下图所示:
avcodec_open2()
的定义位于 libavcodec\utils.c
avcodec_open2()
的源代码量是非常长的,但是它的调用关系非常简单——它只调用了一个关键的函数,即 AVCodec 的 init()
,后文将会对这个函数进行分析。
我们可以简单梳理一下 avcodec_open2()
所做的工作,如下所列:
(1)为各种结构体分配内存(通过各种 av_malloc()
实现)。
(2)将输入的 AVDictionary 形式的选项设置到 AVCodecContext。
(3)其他一些零零碎碎的检查,比如说检查编解码器是否处于 “实验” 阶段。
(4)如果是编码器,检查输入参数是否符合编码器的要求
(5)调用 AVCodec 的 init()
初始化具体的解码器。
前几步比较简单,不再分析。在这里我们分析一下第4步和第5步。
在这里简单分析一下第 4 步,即 “检查输入参数是否符合编码器的要求”。这一步中检查了很多的参数,在这里我们随便选一个参数 pix_fmts(像素格式)看一下,如下所示。
1 | //检查像素格式 |
可以看出,该代码首先进入了一个 for()
循环,将 AVCodecContext 中设定的 pix_fmt
与编码器AVCodec 中的 pix_fmts
数组中的元素逐一比较。
先简单介绍一下 AVCodec 中的 pix_fmts
数组。AVCodec 中的 pix_fmts
数组存储了该种编码器支持的像素格式,并且规定以 AV_PIX_FMT_NONE(AV_PIX_FMT_NONE 取值为 -1)为结尾。例如,libx264 的 pix_fmts
数组的定义位于 libavcodec\libx264.c
,如下所示。
1 | static const enum AVPixelFormat pix_fmts_8bit[] = { |
从 pix_fmts_8bit
的定义可以看出 libx264 主要支持的是以 YUV 为主的像素格式。
现在回到 “检查输入 pix_fmt
是否符合编码器的要求” 的那段代码。如果 for()
循环从 AVCodec->pix_fmts
数组中找到了符合 AVCodecContext->pix_fmt
的像素格式,或者完成了 AVCodec->pix_fmts
数组的遍历,都会跳出循环。如果发现 AVCodec->pix_fmts
数组中索引为 i
的元素是 AV_PIX_FMT_NONE(即最后一个元素,取值为 -1)的时候,就认为没有找到合适的像素格式,并且最终提示错误信息。
avcodec_open2()
中最关键的一步就是调用 AVCodec 的 init()
方法初始化具体的编码器。AVCodec 的 init()
是一个函数指针,指向具体编解码器中的初始化函数。这里我们以 libx264 为例,看一下它对应的 AVCodec 的定义。
libx264 对应的 AVCodec 的定义位于 libavcodec\libx264.c
1 | AVCodec ff_libx264_encoder = { |
可以看出在 ff_libx264_encoder
中 init()
指向 X264_init()
。X264_init()
的定义同样位于libavcodec\libx264.c
X264_init()
的代码以后研究 X264 的时候再进行细节的分析,在这里简单记录一下它做的两项工作:
(1)设置 X264Context 的参数。X264Context 主要完成了 libx264 和 FFmpeg 对接的功能。可以看出代码主要在设置一个 params 结构体变量,该变量的类型即是 x264 中存储参数的结构体 x264_param_t
。(2)调用 libx264 的 API 进行编码器的初始化工作。例如调用 x264_param_default()
设置默认参数,调用 x264_param_apply_profile()
设置 profile,调用 x264_encoder_open()
打开编码器等等。
最后附上 X264Context 的定义,位于 libavcodec\libx264.c
该函数用于关闭编码器。avcodec_close()
函数的声明位于 libavcodec\avcodec.h
1 | int avcodec_close(AVCodecContext *avctx); |
该函数只有一个参数,就是需要关闭的编码器的 AVCodecContext。
函数的调用关系图如下所示:
avcodec_close()
的定义位于 libavcodec\utils.c
从 avcodec_close()
的定义可以看出,该函数释放 AVCodecContext 中有关的变量,并且调用了 AVCodec 的 close()
关闭了解码器。
FFMPEG打开媒体的的过程开始于avformat_open_input,因此该函数的重要性不可忽视。
在该函数中,FFMPEG完成了:
输入输出结构体 AVIOContext 的初始化;
输入数据的协议(例如 RTMP,或者 file)的识别(通过一套评分机制):
使用获得最高分的文件协议对应的 URLProtocol,通过函数指针的方式,与 FFMPEG 连接(非专业用词);
剩下的就是调用该 URLProtocol 的函数进行 open, read 等操作了
以下是通过 eclipse+MinGW 调试 FFMPEG 源代码获得的函数调用关系图:
可见最终都调用了 URLProtocol 结构体中的函数指针。
URLProtocol 结构如下,是一大堆函数指针的集合(avio.h文件)
1 | typedef struct URLProtocol { |
URLProtocol 功能就是完成各种输入协议的读写等操作
但输入协议种类繁多,它是怎样做到 “大一统” 的呢?
原来,每个具体的输入协议都有自己对应的 URLProtocol。
比如 file 协议(FFMPEG 把文件也当做一种特殊的协议)(*file.c
文件)
1 | URLProtocol ff_pipe_protocol = { |
或者rtmp协议(此处使用了librtmp)(librtmp.c文件)
1 | URLProtocol ff_rtmp_protocol = { |
可见它们把各自的函数指针都赋值给了 URLProtocol 结构体的函数指针
因此 avformat_open_input
只需调用 url_open, url_read 这些函数就可以完成各种具体输入协议的 open, read 等操作了
FFMPEG源码分析:avformat_open_input()(媒体打开函数)
avformat_open_input()
个人感觉这个函数确实太重要了,可以算作 FFmpeg 的 “灵魂”
函数用于打开多媒体数据并且获得一些相关的信息。它的声明位于 libavformat\avformat.h
1 | int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options); |
参数说明:
1 | ps:函数调用成功之后处理过的 AVFormatContext 结构体。 |
函数执行成功的话,其返回值大于等于 0。
函数调用结构图如下所示:
avformat_open_input()
定义位于 libavformat\utils.c
中
avformat_open_input()
源代码比较长,一部分是一些容错代码,比如说如果发现传入的 AVFormatContext 指针没有初始化过,就调用 avformat_alloc_context()
初始化该结构体;还有一部分是针对一些格式做的特殊处理,比如 id3v2 信息的处理等等。有关上述两种信息不再详细分析,在这里只选择它关键的两个函数进行分析:
init_input()
:绝大部分初始化工作都是在这里做的。
s->iformat->read_header()
:读取多媒体数据文件头,根据视音频流创建相应的 AVStream。
init_input()
作为一个内部函数,竟然包含了一行注释(一般内部函数都没有注释),足可以看出它的重要性。它的主要工作就是打开输入的视频数据并且探测视频的格式。该函数的定义位于 libavformat\utils.c
1 | /* Open input file and probe the format if necessary. */ |
这个函数在短短的几行代码中包含了好几个 return,因此逻辑还是有点复杂的,我们可以梳理一下:
在函数的开头的 score 变量是一个判决 AVInputFormat 的分数的门限值,如果最后得到的 AVInputFormat 的分数低于该门限值,就认为没有找到合适的 AVInputFormat 。
FFmpeg 内部判断封装格式的原理实际上是对每种 AVInputFormat 给出一个分数,满分是 100 分,越有可能正确的 AVInputFormat 给出的分数就越高。最后选择分数最高的 AVInputFormat 作为推测结果。score 的值是一个宏定义 AVPROBE_SCORE_RETRY,我们可以看一下它的定义:
1 |
其中 AVPROBE_SCORE_MAX 是 score 的最大值,取值是 100:
1 |
由此我们可以得出 score 取值是 25,即如果推测后得到的最佳 AVInputFormat 的分值低于 25,就认为没有找到合适的 AVInputFormat。
整个函数的逻辑大体如下:
(1)当使用了自定义的 AVIOContext 的时候(AVFormatContext 中的 AVIOContext 不为空,即 s->pb!=NULL
),如果指定了 AVInputFormat 就直接返回,如果没有指定就调用 av_probe_input_buffer2()
推测 AVInputFormat。这一情况出现的不算很多,但是当我们从内存中读取数据的时候(需要初始化自定义的 AVIOContext),就会执行这一步骤。
(2)在更一般的情况下,如果已经指定了 AVInputFormat,就直接返回;如果没有指定 AVInputFormat,就调用 av_probe_input_format(NULL,…)
根据文件路径判断文件格式。这里特意把 av_probe_input_format()
的第 1 个参数写成 “NULL”,是为了强调这个时候实际上并没有给函数提供输入数据,此时仅仅通过文件路径推测 AVInputFormat。
(3)如果发现通过文件路径判断不出来文件格式,那么就需要打开文件探测文件格式了,这个时候会首先调用 avio_open2()
打开文件,然后调用 av_probe_input_buffer2()
推测 AVInputFormat。
该函数可以读取一部分视音频数据并且获得一些相关的信息。
avformat_find_stream_info()
的声明位于 libavformat\avformat.h
1 | int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options); |
简单解释一下它的参数的含义:
1 | ic:输入的 AVFormatContext。 |
函数正常执行后返回值大于等于 0。
PS:由于该函数比较复杂,所以只看了一部分代码,以后有时间再进一步分析。
函数的调用关系如下图所示:
avformat_find_stream_info()
的定义位于 libavformat\utils.c
由于avformat_find_stream_info()
代码比较长,难以全部分析,在这里只能简单记录一下它的要点。该函数主要用于给每个媒体流(音频/视频)的 AVStream 结构体赋值。我们大致浏览一下这个函数的代码,会发现它其实已经实现了解码器的查找,解码器的打开,视音频帧的读取,视音频帧的解码等工作。换句话说,该函数实际上已经“走通”的解码的整个流程。下面看一下除了成员变量赋值之外,该函数的几个关键流程。
查找解码器:find_decoder()
打开解码器:avcodec_open2()
读取完整的一帧压缩编码的数据:read_frame_internal()
注:av_read_frame()
内部实际上就是调用的 read_frame_internal()
。
解码一些压缩编码数据:try_decode_frame()
ffmpeg 中的 av_read_frame()
的作用是读取码流中的音频若干帧或者视频一帧。例如,解码视频的时候,每解码一个视频帧,需要先调用 av_read_frame()
获得一帧视频的压缩数据,然后才能对该数据进行解码(例如 H.264 中一帧压缩数据通常对应一个 NAL)。
上代码之前,先参考了其他人对 av_read_frame()
的解释,在此做一个参考:
通过
av_read_packet()
,读取一个包,需要说明的是此函数必须是包含整数帧的,不存在半帧的情况,以 ts 流为例,是读取一个完整的 PES 包(一个完整 pes 包包含若干视频或音频 es 包),读取完毕后,通过av_parser_parse2()
分析出视频一帧(或音频若干帧),返回,下次进入循环的时候,如果上次的数据没有完全取完,则st = s->cur_st
; 不会是 NULL,即再此进入av_parser_parse2()
流程,而不是下面的av_read_packet()
流程,这样就保证了,如果读取一次包含了 N 帧视频数据(以视频为例),则调用av_read_frame()
N 次都不会去读数据,而是返回第一次读取的数据,直到全部解析完毕。
av_read_frame()
的声明位于 libavformat\avformat.h
1 | int av_read_frame(AVFormatContext *s, AVPacket *pkt); |
av_read_frame()
使用方法在注释中写得很详细,用中文简单描述一下它的两个参数:
1 | s:输入的AVFormatContext |
如果返回 0 则说明读取正常。
函数调用结构图如下所示:
av_read_frame()
的定义位于 libavformat\utils.c
read_frame_internal()
代码比较长,这里只简单看一下它前面的部分。它前面部分有 2 步是十分关键的:
(1)调用了 ff_read_packet()
从相应的 AVInputFormat 读取数据。
(2)如果媒体频流需要使用 AVCodecParser,则调用 parse_packet()
解析相应的 AVPacket。
ff_read_packet()
中最关键的地方就是调用了 AVInputFormat 的 read_packet()
方法。 AVInputFormat 的 read_packet()
是一个函数指针,指向当前的 AVInputFormat 的读取数据的函数。在这里我们以 FLV 封装格式对应的 AVInputFormat 为例,看看 read_packet()
的实现函数是什么样子的。
FLV 封装格式对应的 AVInputFormat 的定义位于 libavformat\flvdec.c
1 | AVInputFormat ff_flv_demuxer = { |
从 ff_flv_demuxer
的定义可以看出,read_packet()
对应的是 flv_read_packet()
函数。在看 flv_read_packet()
函数之前,我们先回顾一下 FLV 封装格式的结构,如下图所示。
PS:原图是网上找的,感觉画的很清晰,比官方的 Video File Format Specification 更加通俗易懂。但是图中有一个错误,就是 TagHeader 中的 StreamID 字段的长度写错了(查看了一下官方标准,应该是 3 字节,现在已经改过来了)。
从图中可以看出,FLV 文件体部分是由一个一个的 Tag 连接起来的(中间间隔着 Previous Tag Size)。每个 Tag 包含了 Tag Header 和 Tag Data 两个部分。
Tag Data 根据 Tag 的 Type 不同而不同:可以分为音频 Tag Data,视频 Tag Data 以及 Script Tag Data。下面简述一下音频 Tag Data 和视频 Tag Data。
Audio Tag在官方标准中定义如下。
Audio Tag 开始的第 1 个字节包含了音频数据的参数信息,从第 2 个字节开始为音频流数据。
第 1 个字节的前 4 位的数值表示了音频数据格式:
1 | 0 = Linear PCM, platform endian |
第 1 个字节的第 5-6 位的数值表示采样率:0 = 5.5kHz,1 = 11KHz,2 = 22 kHz,3 = 44 kHz
。
第 1 个字节的第7位表示采样精度:0 = 8bits,1 = 16bits
。
第 1 个字节的第8位表示音频类型:0 = sndMono,1 = sndStereo
。
其中,当音频编码为 AAC 的时候,第一个字节后面存储的是 AACAUDIODATA,格式如下所示。
Video Tag在官方标准中的定义如下:
Video Tag 也用开始的第 1 个字节包含视频数据的参数信息,从第 2 个字节为视频流数据。
第 1 个字节的前 4 位的数值表示帧类型(FrameType):
1 | 1: keyframe (for AVC, a seekableframe)(关键帧) |
第 1 个字节的后 4 位的数值表示视频编码 ID(CodecID):
1 | 1: JPEG (currently unused) |
其中,当音频编码为 AVC(H.264)的时候,第一个字节后面存储的是 AVCVIDEOPACKET,格式如下所示。
了解了 FLV 的基本格式之后,就可以看一下 FLV 解析 Tag 的函数 flv_read_packet()了
。
flv_read_packet()
的定义位于 libavformat\flvdec.c
flv_read_packet()
的代码比较长,但是逻辑比较简单。它的主要功能就是根据 FLV 文件格式的规范,逐层解析 Tag 以及 TagData,获取 Tag 以及 TagData 中的信息。比较关键的地方已经写上了注释,不再详细叙述。
parse_packet()
给需要 AVCodecParser 的媒体流提供解析 AVPacket 的功能。
从代码中可以看出,最终调用了相应 AVCodecParser 的 av_parser_parse2()
函数,解析出来 AVPacket。此后根据解析的信息还进行了一系列的赋值工作,不再详细叙述。
ffmpeg 中的 avcodec_decode_video2()
的作用是解码一帧视频数据。输入一个压缩编码的结构体 AVPacket,输出一个解码后的结构体 AVFrame。该函数的声明位于 libavcodec\avcodec.h
1 | int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture, |
查看源代码之后发现,这个函数竟然十分的简单,源代码位于 libavcodec\utils.c
从代码中可以看出,avcodec_decode_video2()
主要做了以下几个方面的工作:
(1)对输入的字段进行了一系列的检查工作:例如宽高是否正确,输入是否为视频等等。
(2)通过 ret = avctx->codec->decode(avctx, picture, got_picture_ptr,&tmp)
这句代码,调用了相应 AVCodec 的 decode()
函数,完成了解码操作。
(3)对得到的 AVFrame 的一些字段进行了赋值,例如宽高、像素格式等等。
其中第二部是关键的一步,它调用了 AVCodec 的 decode()
方法完成了解码。AVCodec 的 decode()
方法是一个函数指针,指向了具体解码器的解码函数。在这里我们以 H.264 解码器为例,看一下解码的实现过程。H.264 解码器对应的 AVCodec 的定义位于 libavcodec\h264.c
,如下所示。
1 | AVCodec ff_h264_decoder = { |
从 ff_h264_decoder
的定义可以看出,decode()
指向了 h264_decode_frame()
函数。
从 h264_decode_frame()
的定义可以看出,它调用了 decode_nal_units()
完成了具体的 H.264 解码工作。
该函数用于关闭一个 AVFormatContext,一般情况下是和 avformat_open_input()
成对使用的。
函数的调用关系如下图所示:
avformat_close_input()
的源代码位于 libavformat\utils.c
从源代码中可以看出,avformat_close_input()
主要做了以下几步工作:
(1)调用 AVInputFormat 的 read_close()
方法关闭输入流
(2)调用 avformat_free_context()
释放 AVFormatContext
(3)调用 avio_close()
关闭并且释放 AVIOContext
在基于 FFmpeg 的视音频编码器程序中,该函数通常是第一个调用的函数(除了组件注册函数 av_register_all()
)。
avformat_alloc_output_context2()
函数可以初始化一个用于输出的 AVFormatContext 结构体。它的声明位于 libavformat\avformat.h
1 | int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, |
代码中的英文注释写的已经比较详细了,在这里拿中文简单叙述一下。
1 | ctx:函数调用成功之后创建的AVFormatContext结构体。 |
函数执行成功的话,其返回值大于等于0。
首先贴出来最终分析得出的函数调用结构图,如下所示:
avformat_alloc_output_context2()
的函数定义位于 libavformat\mux.c
从代码中可以看出,avformat_alloc_output_context2()
的流程如要包含以下 2 步:
1) 调用 avformat_alloc_context()
初始化一个默认的 AVFormatContext。
2) 如果指定了输入的 AVOutputFormat,则直接将输入的 AVOutputFormat 赋值给AVOutputFormat 的 oformat。如果没有指定输入的 AVOutputFormat,就需要根据文件格式名称或者文件名推测输出的 AVOutputFormat。无论是通过文件格式名称还是文件名推测输出格式,都会调用一个函数 av_guess_format()
。
avformat_alloc_context()
首先调用 av_malloc()
为 AVFormatContext 分配一块内存。然后调用了一个函数 avformat_get_context_defaults()
用于给 AVFormatContext 设置默认值
avformat_alloc_context()
首先调用 memset()
将 AVFormatContext 的内存置零;然后指定它的AVClass(指定了 AVClass 之后,该结构体就支持和 AVOption 相关的功能);最后调用 av_opt_set_defaults()
给 AVFormatContext 的成员变量设置默认值(av_opt_set_defaults()
就是和 AVOption 有关的一个函数,专门用于给指定的结构体设定默认值,此处暂不分析)。
av_guess_format()
中使用一个整型变量 score 记录每种输出格式的匹配程度。函数中包含了一个 while()
循环,该循环利用函数 av_oformat_next()
遍历 FFmpeg 中所有的 AVOutputFormat,并逐一计算每个输出格式的 score。具体的计算过程分成如下几步:
1) 如果封装格式名称匹配,score 增加 100。匹配中使用了函数 av_match_name()
。
2) 如果 mime 类型匹配,score 增加 10。匹配直接使用字符串比较函数 strcmp()
。
3) 如果文件名称的后缀匹配,score 增加 5。匹配中使用了函数 av_match_ext()
。
while()
循环结束后,得到得分最高的格式,就是最匹配的格式。
下面看一下一个 AVOutputFormat 的实例,就可以理解 “封装格式名称”,“mine类型”,“文件名称后缀” 这些概念了。下面是 flv 格式的视音频复用器(Muxer)对应的 AVOutputFormat 格式的变量 ff_flv_muxer
。
1 | AVOutputFormat ff_flv_muxer = { |
FFmpeg 的写文件用到的 3 个函数:
avformat_write_header()
av_write_frame()
av_write_trailer()
其中 av_write_frame()
用于写视频数据,avformat_write_header()
用于写视频文件头,而 av_write_trailer()
用于写视频文件尾。
本文首先分析avformat_write_header()
。
PS:需要注意的是,尽管这 3 个函数功能是配套的,但是它们的前缀却不一样,写文件头 Header 的函数前缀是“avformat_
”,其他两个函数前缀是“av_
”(不太明白其中的原因)。
avformat_write_header()
的声明位于 libavformat\avformat.h
1 | int avformat_write_header(AVFormatContext *s, AVDictionary **options); |
简单解释一下它的参数的含义:
1 | s:用于输出的AVFormatContext。 |
函数正常执行后返回值等于 0。
avformat_write_header()
的调用关系如下图所示:
avformat_write_header()
的定义位于 libavformat\mux.c
从源代码可以看出,avformat_write_header()
完成了以下工作:
(1)调用 init_muxer()
初始化复用器
(2)调用 AVOutputFormat 的 write_header()
init_muxer()
代码很长,但是它所做的工作比较简单,可以概括成两个字:检查。函数的流程可以概括成以下几步:
(1)将传入的 AVDictionary 形式的选项设置到 AVFormatContext
(2)遍历 AVFormatContext 中的每个 AVStream,并作如下检查:
a) AVStream 的 time_base 是否正确设置。如果发现 AVStream 的 time_base 没有设置,则会调用 avpriv_set_pts_info()
进行设置。
b) 对于音频,检查采样率设置是否正确;对于视频,检查宽、高、宽高比。
c) 其他一些检查,不再详述。
AVOutputFormat->write_header()
avformat_write_header()
中最关键的地方就是调用了 AVOutputFormat 的 write_header()
。
write_header()
是 AVOutputFormat 中的一个函数指针,指向写文件头的函数。不同的AVOutputFormat 有不同的 write_header()
的实现方法。在这里我们举例子看一下 FLV 封装格式对应的 AVOutputFormat,它的定义位于 libavformat\flvenc.c
从 ff_flv_muxer
的定义中可以看出,write_header()
指向的函数为 flv_write_header()
。我们继续看一下 flv_write_header()
函数。flv_write_header()
的定义同样位于 libavformat\flvenc.c
从源代码可以看出,flv_write_header()
完成了FLV文件头的写入工作。该函数的工作可以大体分为以下两部分:
(1)给 FLVContext 设置参数
(2)写文件头,以及相关的 Tag
可以参考下图中 FLV 文件头的定义比对一下上面的代码。
该函数用于编码一帧视频数据。avcodec_encode_video2()
函数的声明位于 libavcodec\avcodec.h
1 | int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt, |
该函数每个参数的含义在注释里面已经写的很清楚了,在这里用中文简述一下:
1 | avctx:编码器的AVCodecContext。 |
函数返回0代表编码成功。
函数的调用关系如下图所示:
avcodec_encode_video2()
的定义位于 libavcodec\utils.c
从函数的定义可以看出,avcodec_encode_video2()
首先调用了 av_image_check_size()
检查设置的宽高参数是否合理,然后调用了 AVCodec 的 encode2()
调用具体的解码器。
av_image_check_size()
主要是要求图像宽高必须为正数,而且取值不能太大。
AVCodec 的 encode2()
是一个函数指针,指向特定编码器的编码函数
从 ff_libx264_encoder
的定义可以看出,encode2()
函数指向的是 X264_frame()
函数。
X264_frame()
函数的定义位于 libavcodec\libx264.c
av_write_frame()
用于输出一帧视音频数据,它的声明位于 libavformat\avformat.h
1 | int av_write_frame(AVFormatContext *s, AVPacket *pkt); |
简单解释一下它的参数的含义:
1 | s:用于输出的AVFormatContext。 |
函数正常执行后返回值等于 0。
av_write_frame()
的调用关系如下图所示:
av_write_frame()
的定义位于 libavformat\mux.c
从源代码可以看出,av_write_frame()
主要完成了以下几步工作:
(1)调用 check_packet()
做一些简单的检测
(2)调用 compute_pkt_fields2()
设置 AVPacket 的一些属性值
(3)调用 write_packet()
写入数据
check_packet()
的功能比较简单:首先检查一下输入的 AVPacket 是否为空,如果为空,则是直接返回;然后检查一下 AVPacket 的 stream_index
(标记了该 AVPacket 所属的 AVStream)设置是否正常,如果为负数或者大于 AVStream 的个数,则返回错误信息;最后检查 AVPacket 所属的 AVStream 是否属于 attachment stream,这个地方没见过,目前还没有研究。
compute_pkt_fields2()
函数的定义位于 libavformat\mux.c
compute_pkt_fields2()
主要有两方面的功能:
write_packet()
函数的定义位于 libavformat\mux.c
write_packet()
函数最关键的地方就是调用了 AVOutputFormat 中写入数据的方法。如果 AVPacket 中的 flag 标记中包含 AV_PKT_FLAG_UNCODED_FRAME,就会调用 AVOutputFormat 的 write_uncoded_frame()
函数;如果不包含那个标记,就会调用 write_packet()
函数。 write_packet()
实际上是一个函数指针,指向特定的 AVOutputFormat 中的实现函数。例如,我们看一下 FLV 对应的 AVOutputFormat,位于 libavformat\flvenc.c
从 ff_flv_muxer
的定义可以看出,write_packet()
指向的是 flv_write_packet()
函数。在看 flv_write_packet()
函数的定义之前,先回顾一下 FLV 封装格式的结构。
av_write_trailer()
用于输出文件尾,它的声明位于 libavformat\avformat.h
1 | int av_write_trailer(AVFormatContext *s); |
它只需要指定一个参数,即用于输出的 AVFormatContext。
函数正常执行后返回值等于 0。
av_write_trailer()
的调用关系如下图所示:
av_write_trailer()
的定义位于 libavformat\mux.c
从源代码可以看出 av_write_trailer()
主要完成了以下两步工作:
(1)循环调用 interleave_packet()
以及 write_packet()
,将还未输出的 AVPacket 输出出来。
(2)调用 AVOutputFormat 的 write_trailer()
,输出文件尾。
其中第一步和 av_write_frame()
中的步骤大致是一样的(interleave_packet()
这一部分在并不包含在 av_write_frame()
中,而是包含在 av_interleaved_write_frame()
中,这一部分源代码还没有分析)
AVOutputFormat 的 write_trailer()
是一个函数指针,指向特定的 AVOutputFormat 中的实现函数。我们以 FLV 对应的 AVOutputFormat 为例,看一下它的定义
从 FLV 对应的 AVOutputFormat 结构体的定义我们可以看出,write_trailer()
指向了flv_write_trailer()
函数。
flv_write_trailer()
函数的定义位于 libavformat\flvenc.c
从 flv_write_trailer()
的源代码可以看出该函数做了以下两步工作:
(1)如果视频流是 H.264,则添加包含 EOS(End Of Stream) NALU 的 Tag。
(2)更新 FLV 的时长信息,以及文件大小信息。
其中,put_avc_eos_tag()
函数用于添加包含 EOS NALU 的 Tag(包含结尾的一个PreviousTagSize)
可以参考 FLV 封装格式理解上述函数。由于前面的文章中已经描述过 FLV 封装格式,在这里不再重复叙述,在这里仅在此记录一下 AVCVIDEOPACKET 的格式,如下所示。
可以看出包含 EOS NALU 的 AVCVIDEOPACKET 的 AVCPacketType 为 2。在这种情况下, AVCVIDEOPACKET 的 CompositionTime 字段取 0,并且无需包含 Data 字段。
日志输出系统
本文分析一下 FFmpeg 的日志(Log)输出系统的源代码。日志输出部分的核心函数只有一个: av_log()
。使用 av_log()
在控制台输出日志的效果如下图所示。
FFmpeg 日志输出系统的函数调用结构图如图所示:
av_log()
是 FFmpeg 中输出日志的函数。随便打开一个 FFmpeg 的源代码文件,就会发现其中遍布着 av_log()
函数。一般情况下 FFmpeg 类库的源代码中是不允许使用 printf()
这种的函数的,所有的输出一律使用 av_log()
。
av_log()的声明位于libavutil\log.h
1 | void av_log(void *avcl, int level, const char *fmt, ...) av_printf_format(3, 4); |
这个函数的声明有两个地方比较特殊:
(1)函数最后一个参数是 “…”。
在 C 语言中,在函数参数数量不确定的情况下使用 “…” 来代表参数。例如 printf()
的原型定义如下
1 | int printf (const char*, ...); |
(2)它的声明后面有一个 av_printf_format(3, 4)
。有关这个地方的左右还没有深入研究,网上资料中说它的作用是按照 printf()
的格式检查 av_log()
的格式。
av_log()每个字段的含义如下:
printf()
一样。由此可见,av_log()
和 printf()
的不同主要在于前面多了两个参数。其中第一个参数指定该 log 所属的结构体,例如 AVFormatContext、AVCodecContext 等等。第二个参数指定 log 的级别,源代码中定义了如下几个级别。
1 |
从定义中可以看出来,随着严重程度逐渐下降,一共包含如下级别:
每个级别定义的数值代表了严重程度,数值越小代表越严重。默认的级别是 AV_LOG_INFO。此外,还有一个级别不输出任何信息,即 AV_LOG_QUIET。
当前系统存在着一个 “Log级别”。所有严重程度高于该级别的 Log 信息都会输出出来。例如当前的 Log 级别是 AV_LOG_WARNING,则会输出 AV_LOG_PANIC,AV_LOG_FATAL,AV_LOG_ERROR,AV_LOG_WARNING 级别的信息,而不会输出 AV_LOG_INFO 级别的信息。可以通过 av_log_get_level()
获得当前 Log 的级别,通过另一个函数 av_log_set_level()
设置当前的 Log 级别。
可以通过 av_log_set_level()
设置当前 Log 的级别。
FFmpeg源代码简单分析:结构体成员管理系统-AVClass
TODO
FFmpeg源代码简单分析:结构体成员管理系统-AVOption
TODO
FFmpeg源代码简单分析:libswscale的sws_getContext()
TODO
FFmpeg源代码简单分析:libswscale的sws_scale()
TODO
FFmpeg源代码简单分析:libavdevice的avdevice_register_all()
]]>FFmpeg源代码简单分析:libavdevice的gdigrab
特别说明,此文参考至雷神笔记,做一个备忘录。
下图表明了 FFmpeg 在解码一个视频的时候的函数调用流程。为了保证结构清晰,其中仅列出了最关键的函数,剔除了其它不是特别重要的函数。
下面解释一下图中关键标记的含义。
函数在图中以方框的形式表现出来。不同的背景色标志了该函数不同的作用:
PS:URLProtocol,AVInputFormat,AVCodec在FFmpeg开始运行并且注册完组件之后,都会分别被连接成一个个的链表。因此实际上是有很多的URLProtocol,AVInputFormat,AVCodec的。图中画出了解码一个输入协议是“文件”(其实就是打开一个文件。“文件”也被当做是一种广义的协议),封装格式为FLV,视频编码格式是H.264的数据的函数调用关系。
整个架构图可以分为以下几个区域:
为了把调用关系表示的更明显,图中的箭头线也使用了不同的颜色:
黑色箭头线:标志了函数之间的调用关系。
红色的箭头线:标志了解码的流程。
其他颜色的箭头线:标志了函数之间的调用关系。其中:
每个函数旁边标识了它所在的文件的路径。
此外,还有一点需要注意的是,一些 API 函数内部也调用了另一些API函数。也就是说,API函数并不一定全部都调用FFmpeg的内部函数,他也有可能调用其他的API函数。例如从图中可以看出来, avformat_close_input()
调用了 avformat_free_context()
和 avio_close()
。这些在内部代码中被调用的API函数也标记为粉红色。
下面简单列出几个区域中函数之间的调用关系(函数之间的调用关系使用缩进的方式表现出来)。详细的函数分析可以参考相关的《FFmpeg源代码分析》系列文章。
1. av_register_all()【函数简单分析】>
2. avformat_alloc_context()【函数简单分析】
1) av_malloc(sizeof(AVFormatContext))
2) avformat_get_context_defaults()
3. avformat_open_input()【函数简单分析】
4. avformat_find_stream_info()【函数简单分析】
5. avcodec_find_decoder()【函数简单分析】
6. avcodec_open2()【函数简单分析】
7. av_read_frame()【函数简单分析】
1) read_from_packet_buffer()
2) read_frame_internal()
8. avcodec_decode_video2()【函数简单分析】
1) av_packet_split_side_data()
2) AVCodec-> decode()
3) av_frame_set_pkt_pos()
4) av_frame_set_best_effort_timestamp()
9. avcodec_close()【函数简单分析】
10. avformat_close_input()【函数简单分析】
1) AVInputFormat->read_close()
2) avformat_free_context()
3) avio_close()
URLProtocol结构体包含如下协议处理函数指针:
【例子】不同的协议对应着上述接口有不同的实现函数,举几个例子:
File协议(即文件)对应的URLProtocol结构体 ff_file_protocol
:
1 | url_open() -> file_open() -> open() |
RTMP协议(libRTMP)对应的URLProtocol结构体 ff_librtmp_protocol
:
1 | url_open() -> rtmp_open() -> RTMP_Init(), RTMP_SetupURL(), RTMP_Connect(), RTMP_ConnectStream() |
UDP协议对应的URLProtocol结构体 ff_udp_protocol
:
1 | url_open() -> udp_open() |
AVInputFormat包含如下封装格式处理函数指针:
【例子】不同的封装格式对应着上述接口有不同的实现函数,举几个例子:
FLV封装格式对应的AVInputFormat结构体 ff_flv_demuxer
:
1 | read_probe() -> flv_probe() –> probe() |
MKV封装格式对应的AVInputFormat结构体 ff_matroska_demuxer
:
1 | read_probe() -> matroska_probe() |
MPEG2TS封装格式对应的AVInputFormat结构体 ff_mpegts_demuxer
:
1 | read_probe() -> mpegts_probe() |
AVI封装格式对应的AVInputFormat结构体 ff_avi_demuxer
:
1 | read_probe() -> avi_probe() |
AVCodec包含如下编解码函数指针:
【例子】不同的编解码器对应着上述接口有不同的实现函数,举几个例子:
HEVC解码对应的AVCodec结构体 ff_hevc_decoder
:
1 | init() -> hevc_decode_init() |
H.264解码对应的AVCodec结构体 ff_h264_decoder
:
1 | init() -> ff_h264_decode_init() |
VP8解码(libVPX)对应的AVCodec结构体 ff_libvpx_vp8_decoder
:
1 | init() -> vpx_init() -> vpx_codec_dec_init() |
MPEG2解码对应的AVCodec结构体 ff_mpeg2video_decoder
:
1 | init() -> mpeg_decode_init() |
下图表明了FFmpeg在编码一个视频的时候的函数调用流程。为了保证结构清晰,其中仅列出了最关键的函数,剔除了其它不是特别重要的函数。
下面解释一下图中关键标记的含义。
函数在图中以方框的形式表现出来。不同的背景色标志了该函数不同的作用:
整个关系图可以分为以下几个区域:
为了把调用关系表示的更明显,图中的箭头线也使用了不同的颜色:
红色的箭头线:标志了编码的流程。
其他颜色的箭头线:标志了函数之间的调用关系。其中:
每个函数标识了它所在的文件路径。
下面简单列出几个区域中函数之间的调用关系(函数之间的调用关系使用缩进的方式表现出来)。详细的函数分析可以参考相关的《FFmpeg源代码分析》系列文章。
1. av_register_all()【函数简单分析】
1) avcodec_register_all()
2) REGISTER_MUXER()
3) REGISTER_DEMUXER()
4) REGISTER_PROTOCOL()
2. avformat_alloc_output_context2()【函数简单分析】
1) avformat_alloc_context()
(a) av_malloc(sizeof(AVFormatContext))
(b) avformat_get_context_defaults()
2) av_guess_format()
3. avio_open2()【函数简单分析】
1) ffurl_open()
2) ffio_fdopen()
4. avformat_new_stream()【函数简单分析】
1) av_mallocz(sizeof(AVStream))
2) avcodec_alloc_context3()
5. avcodec_find_encoder()【函数简单分析】
6. avcodec_open2()【函数简单分析】
7. avformat_write_header()【函数简单分析】
1) init_muxer()
2) AVOutputFormat->write_header()
3) init_pts()
8. avcodec_encode_video2()【函数简单分析】
9. av_write_frame()【函数简单分析】
1) check_packet()
2) compute_pkt_fields2()
3) write_packet()
10. av_write_trailer()【函数简单分析】
1) write_packet()
2) AVOutputFormat->write_trailer()
11. avcodec_close()【函数简单分析】
12. avformat_free_context()【函数简单分析】
13. avio_close()【函数简单分析】
URLProtocol结构体包含如下协议处理函数指针:
【例子】不同的协议对应着上述接口有不同的实现函数,举几个例子:
File协议(即文件)对应的URLProtocol结构体 ff_file_protocol
:
1 | url_open() -> file_open() -> open() |
RTMP协议(libRTMP)对应的URLProtocol结构体 ff_librtmp_protocol
:
1 | url_open() -> rtmp_open() -> RTMP_Init(), RTMP_SetupURL(), RTMP_Connect(), RTMP_ConnectStream() |
UDP协议对应的URLProtocol结构体 ff_udp_protocol
:
1 | url_open() -> udp_open() |
AVOutputFormat包含如下封装格式处理函数指针:
【例子】不同的封装格式对应着上述接口有不同的实现函数,举几个例子:
FLV封装格式对应的AVOutputFormat结构体 ff_flv_muxer
:
1 | write_header() -> flv_write_header() |
MKV封装格式对应的AVOutputFormat结构体 ff_matroska_muxer
:
1 | write_header() -> mkv_write_header() |
MPEG2TS封装格式对应的AVOutputFormat结构体 ff_mpegts_muxer
:
1 | write_header() -> mpegts_write_header() |
AVI封装格式对应的AVOutputFormat结构体 ff_avi_muxer
:
1 | write_header() -> avi_write_header() |
AVCodec包含如下编解码函数指针:
【例子】不同的编解码器对应着上述接口有不同的实现函数,举几个例子:
HEVC编码器对应的AVCodec结构体 ff_libx265_encoder
:
1 | init() -> libx265_encode_init() -> x265_param_alloc(), x265_param_default_preset(), x265_encoder_open() |
H.264编码器对应的AVCodec结构体 ff_libx264_encoder
:
1 | init() -> X264_init() -> x264_param_default(), x264_encoder_open(), x264_encoder_headers() |
VP8编码器(libVPX)对应的AVCodec结构体 ff_libvpx_vp8_encoder
:
1 | init() -> vpx_init() -> vpx_codec_enc_config_default() |
MPEG2编码器对应的AVCodec结构体 ff_mpeg2video_encoder
:
1 | init() -> encode_init() |
基于FFmpeg进行RTMP推流(二)
实战 - 基于ffmpeg,qt5,opencv视频课程
rtmp 延时一般 1-3 秒
1 | apt-get install wget cmake |
1 | error |
1 | wget https://nginx.org/download/nginx-1.16.0.tar.gz --no-check-certificate |
1 | nginx.conf 配置 |
1 | 推流命令 |
1 | 网页查看推流的状态 |
1 | Reload config: |
Mac源码安装使用OpenCV
在MacOS 10.13.2 下编译 OpenCV3.4.0 + OpenCV Contrib 3.4.0 成 Java 库
在MacOS上安装OpenCV 3.4(c++)
1 | 下载 OpenCV 3.4.0 |
QT音频录制接口:
直播推流要求实时性,一秒钟25帧,做美颜的总耗时一定要低于40ms(每帧消耗40ms)
现在视频推流一般都是1280 X 720
手机端是基于GPU 第三方库做的计算
美颜算法一般都是基于GPU做的
头文件尽量不用引用命名空间,因为不知道谁来调用,可能会出现问题。
头文件中尽量不要引用第三方库文件,应为涉及到第三方库版本升级之类的,第三方头文件的引用应该在代码中引用。
1. RTP:
参考文档 RFC3550/RFC3551
(Real-time Transport Protocol) 是用于 Internet 上针对多媒体数据流的一种传输层协议。RTP 协议详细说明了在互联网上传递音频和视频的标准数据包格式。RTP 协议常用于流媒体系统(配合 RTCP协议),视频会议和一键通(Push to Talk)系统(配合 H.323 或 SIP),使它成为 IP 电话产业的技术基础。RTP 协议和 RTP 控制协议 RTCP 一起使用,而且它是建立在 UDP 协议上的。
RTP 本身并没有提供按时发送机制或其它服务质量(QoS)保证,它依赖于低层服务去实现这一过程。 RTP 并不保证传送或防止无序传送,也不确定底层网络的可靠性。 RTP 实行有序传送, RTP 中的序列号允许接收方重组发送方的包序列,同时序列号也能用于决定适当的包位置,例如:在视频解码中,就不需要顺序解码。
RTP 由两个紧密链接部分组成:
2. RTCP
实时传输控制协议(Real-time Transport Control Protocol 或 RTP Control Protocol 或简写 RTCP)是实时传输协议(RTP)的一个姐妹协议。RTCP 为 RTP 媒体流提供信道外(out-of-band)控制。RTCP 本身并不传输数据,但和 RTP 一起协作将多媒体数据打包和发送。RTCP 定期在流多媒体会话参加者之间传输控制数据。RTCP 的主要功能是为 RTP 所提供的服务质量(Quality of Service)提供反馈。
RTCP 收集相关媒体连接的统计信息,例如:传输字节数,传输分组数,丢失分组数,jitter,单向和双向网络延迟等等。网络应用程序可以利用 RTCP 所提供的信息试图提高服务质量,比如限制信息流量或改用压缩比较小的编解码器。RTCP 本身不提供数据加密或身份认证。SRTCP 可以用于此类用途。
3. SRTP & SRTCP
参考文档 RFC3711
安全实时传输协议(Secure Real-time Transport Protocol 或 SRTP)是在实时传输协议(Real-time Transport Protocol 或 RTP)基础上所定义的一个协议,旨在为单播和多播应用程序中的实时传输协议的数据提供加密、消息认证、完整性保证和重放保护。它是由 David Oran(思科)和 Rolf Blom(爱立信)开发的,并最早由 IETF 于 2004年3月作为 RFC3711 发布。
由于实时传输协议和可以被用来控制实时传输协议的会话的实时传输控制协议(RTP Control Protocol 或 RTCP)有着紧密的联系,安全实时传输协议同样也有一个伴生协议,它被称为安全实时传输控制协议(Secure RTCP 或 SRTCP);安全实时传输控制协议为实时传输控制协议提供类似的与安全有关的特性,就像安全实时传输协议为实时传输协议提供的那些一样。
在使用实时传输协议或实时传输控制协议时,使不使用安全实时传输协议或安全实时传输控制协议是可选的;但即使使用了安全实时传输协议或安全实时传输控制协议,所有它们提供的特性(如加密和认证)也都是可选的,这些特性可以被独立地使用或禁用。唯一的例外是在使用安全实时传输控制协议时,必须要用到其消息认证特性。
4. RTSP
参考文档 RFC2326
是由 Real Networks 和 Netscape 共同提出的。该协议定义了一对多应用程序如何有效地通过 IP 网络传送多媒体数据。RTSP 提供了一个可扩展框架,使实时数据,如音频与视频的受控、点播成为可能。数据源包括现场数据与存储在剪辑中的数据。该协议目的在于控制多个数据发送连接,为选择发送通道,如UDP、多播UDP与TCP提供途径,并为选择基于RTP上发送机制提供方法。
RTSP(Real Time Streaming Protocol)是用来控制声音或影像的多媒体串流协议,并允许同时多个串流需求控制,传输时所用的网络通讯协定并不在其定义的范围内,服务器端可以自行选择使用 TCP 或 UDP来传送串流内容,它的语法和运作跟 HTTP 1.1 类似,但并不特别强调时间同步,所以比较能容忍网络延迟。而前面提到的允许同时多个串流需求控制(Multicast),除了可以降低服务器端的网络用量,更进而支持多方视讯会议(Video Conference)。 因为与 HTTP1.1 的运作方式相似,所以代理服务器《Proxy》的快取功能《Cache》也同样适用于 RTSP,并因 RTSP 具有重新导向功能,可视实际负载情况来转换提供服务的服务器,以避免过大的负载集中于同一服务器而造成延迟。
5. RTSP 和 RTP 的关系
RTP 不象 http 和 ftp 可完整的下载整个影视文件,它是以固定的数据率在网络上发送数据,客户端也是按照这种速度观看影视文件,当影视画面播放过后,就不可以再重复播放,除非重新向服务器端要求数据。
RTSP 与 RTP 最大的区别在于:RTSP 是一种双向实时数据传输协议,它允许客户端向服务器端发送请求,如回放、快进、倒退等操作。当然,RTSP 可基于 RTP 来传送数据,还可以选择 TCP、UDP、组播 UDP 等通道来发送数据,具有很好的扩展性。它是一种类似与 http 协议的网络应用层协议。目前碰到的一个应用:服务器端实时采集、编码并发送两路视频,客户端接收并显示两路视频。由于客户端不必对视频数据做任何回放、倒退等操作,可直接采用 UDP + RTP + 组播实现。
RTP:实时传输协议(Real-time Transport Protocol)
RTP/RTCP 是实际传输数据的协议
RTP 传输音频/视频数据,如果是 PLAY,Server 发送到 Client 端,如果是 RECORD,可以由Client 发送到 Server
整个 RTP 协议由两个密切相关的部分组成:
RTSP:实时流协议(Real Time Streaming Protocol,RTSP)
RTSP 的请求主要有 DESCRIBE, SETUP, PLAY, PAUSE, TEARDOWN, OPTIONS 等,顾名思义可以知道起对话和控制作用
RTSP 的对话过程中 SETUP 可以确定 RTP/RTCP 使用的端口,PLAY/PAUSE/TEARDOWN 可以开始或者停止 RTP 的发送,等等
RTCP:
6. SDP
会话描述协议(SDP)为会话通知、会话邀请和其它形式的多媒体会话初始化等目的提供了多媒体会话描述。
会话目录用于协助多媒体会议的通告,并为会话参与者传送相关设置信息。SDP 即用于将这种信息传输到接收端。SDP 完全是一种会话描述格式 ― 它不属于传输协议 ― 它只使用不同的适当的传输协议,包括会话通知协议(SAP)、会话初始协议(SIP)、实时流协议(RTSP)、MIME 扩展协议的电子邮件以及超文本传输协议(HTTP)。
SDP 的设计宗旨是通用性,它可以应用于大范围的网络环境和应用程序,而不仅仅局限于组播会话目录,但 SDP 不支持会话内容或媒体编码的协商。
在因特网组播骨干网(Mbone)中,会话目录工具被用于通告多媒体会议,并为参与者传送会议地址和参与者所需的会议特定工具信息,这由 SDP 完成。SDP 连接好会话后,传送足够的信息给会话参与者。SDP 信息发送利用了会话通知协议(SAP),它周期性地组播通知数据包到已知组播地址和端口处。这些信息是 UDP 数据包,其中包含 SAP 协议头和文本有效载荷(text payload)。这里文本有效载荷指的是 SDP 会话描述。此外信息也可以通过电子邮件或 WWW (World Wide Web) 进行发送。
SDP 文本信息包括:
SDP 信息是文本信息,采用 UTF-8 编 码中的 ISO 10646 字符集。SDP 会话描述如下:(标注 * 符号的表示可选字段):
1 | v = (协议版本) |
一个或更多时间描述(如下所示):
1 | z = * (时间区域调整) |
0个或多个媒体描述(如下所示)
时间描述
1 | t = (会话活动时间) |
媒体描述
1 | m = (媒体名称和传输地址) |
7. RTMP/RTMPS
RTMP(Real Time Messaging Protocol) 实时消息传送协议是 Adobe Systems 公司为 Flash 播放器和服务器之间音频、视频和数据传输 开发的开放协议。
它有三种变种:
1) 工作在 TCP 之上的明文协议,使用端口1935;
2) RTMPT 封装在 HTTP 请求之中,可穿越防火墙;
3) RTMPS 类似 RTMPT,但使用的是 HTTPS 连接;
RTMP 协议(Real Time Messaging Protocol)是被 Flash 用于对象, 视频, 音频的传输. 这个协议建立在 TCP 协议或者轮询 HTTP 协议之上.
RTMP 协议就像一个用来装数据包的容器, 这些数据既可以是 AMF 格式的数据,也可以是 FLV 中的视/音频数据. 一个单一的连接可以通过不同的通道传输多路网络流. 这些通道中的包都是按照固定大小的包传输的.
8. mms
MMS (Microsoft Media Server Protocol),中文“微软媒体服务器协议”,用来访问并流式接收 Windows Media 服务器中 .asf
文件的一种协议。MMS 协议用于访问 Windows Media 发布点上的单播内容。MMS 是连接 Windows Media 单播服务的默认方法。若观众在 Windows Media Player 中键入一个 URL 以连接内容,而不是通过超级链接访问内容,则他们必须使用MMS 协议引用该流。MMS的预设埠(端口)是1755
当使用 MMS 协议连接到发布点时,使用协议翻转以获得最佳连接。“协议翻转”始于试图通过 MMSU 连接客户端。 MMSU 是 MMS 协议结合 UDP 数据传送。如果 MMSU 连接不成功,则服务器试图使用 MMST。MMST 是 MMS 协议结合 TCP 数据传送。
如果连接到编入索引的 .asf
文件,想要快进、后退、暂停、开始和停止流,则必须使用 MMS。不能用 UNC 路径快进或后退。若您从独立的 Windows Media Player 连接到发布点,则必须指定单播内容的 URL。若内容在主发布点点播发布,则 URL 由服务器名和 .asf
文件名组成。例如:mms://windows_media_server/sample.asf
。其中 windows_media_server 是 Windows Media 服务器名,sample.asf 是您想要使之转化为流的 .asf
文件名。
若您有实时内容要通过广播单播发布,则该 URL 由服务器名和发布点别名组成。例如:mms://windows_media_server/LiveEvents
。这里 windows_media_server 是 Windows Media 服务器名,而 LiveEvents 是发布点名
9. HLS
HTTP Live Streaming(HLS)是苹果公司(Apple Inc.)实现的基于HTTP的流媒体传输协议,可实现流媒体的直播和点播,主要应用在 iOS 系统,为 iOS 设备(如iPhone、iPad)提供音视频直播和点播方案。HLS 点播,基本上就是常见的分段 HTTP 点播,不同在于,它的分段非常小。
相对于常见的流媒体直播协议,例如 RTMP协议、RTSP协议、MMS协议等,HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。HLS 协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式),而客户端则不断的下载并播放这些小文件,因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。由此可见,基本上可以认为,HLS 是以点播的技术方式来实现直播。由于数据通过 HTTP 协议传输,所以完全不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过 HLS 的这种技术特点,决定了它的延迟一般总是会高于普通的流媒体直播协议。
根据以上的了解要实现 HTTP Live Streaming 直播,需要研究并实现以下技术关键点:
HLS ( HTTP Live Streaming)苹果公司提出的流媒体协议,直接把流媒体切片成一段段,信息保存到m3u列表文件中,可以将不同速率的版本切成相应的片;播放器可以直接使用http协议请求流数据,可以在不同速率的版本间自由切换,实现无缝播放;省去使用其他协议的烦恼。缺点是延迟大小受切片大小影响,不适合直播,适合视频点播。
RTSP (Real-Time Stream Protocol)由Real Networks 和 Netscape共同提出的,基于文本的多媒体播放控制协议。RTSP定义流格式,流数据经由RTP传输;RTSP实时效果非常好,适合视频聊天,视频监控等方向。
RTMP(Real Time Message Protocol) 有 Adobe 公司提出,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题,优势在于低延迟,稳定性高,支持所有摄像头格式,浏览器加载 flash插件就可以直接播放。
总结:HLS 延迟大,适合视频点播;RTSP虽然实时性最好,但是实现复杂,适合视频聊天和视频监控;RTMP强在浏览器支持好,加载flash插件后就能直接播放,所以非常火,相反在浏览器里播放rtsp就很困难了。
1:RTSP实时流协议
作为一个应用层协议,RTSP提供了一个可供扩展的框架,它的意义在于使得实时流媒体数据的受控和点播变得可能。总的说来,RTSP是一个流媒体表示 协议,主要用来控制具有实时特性的数据发送,但它本身并不传输数据,而是必须依赖于下层传输协议所提供的某些服务。RTSP可以对流媒体提供诸如播放、暂 停、快进等操作,它负责定义具体的控制消息、操作方法、状态码等,此外还描述了与RTP间的交互操作(RFC2326)。
2:RTCP控制协议
RTCP控制协议需要与RTP数据协议一起配合使用,当应用程序启动一个RTP会话时将同时占用两个端口,分别供RTP和RTCP使用。RTP本身并 不能为按序传输数据包提供可靠的保证,也不提供流量控制和拥塞控制,这些都由RTCP来负责完成。通常RTCP会采用与RTP相同的分发机制,向会话中的 所有成员周期性地发送控制信息,应用程序通过接收这些数据,从中获取会话参与者的相关资料,以及网络状况、分组丢失概率等反馈信息,从而能够对服务质量进 行控制或者对网络状况进行诊断。
RTCP协议的功能是通过不同的RTCP数据报来实现的,主要有如下几种类型:
SR:发送端报告,所谓发送端是指发出RTP数据报的应用程序或者终端,发送端同时也可以是接收端。(SERVER定时间发送给CLIENT)。
RR:接收端报告,所谓接收端是指仅接收但不发送RTP数据报的应用程序或者终端。(SERVER接收CLIENT端发送过来的响应)。
SDES:源描述,主要功能是作为会话成员有关标识信息的载体,如用户名、邮件地址、电话号码等,此外还具有向会话成员传达会话控制信息的功能。
BYE:通知离开,主要功能是指示某一个或者几个源不再有效,即通知会话中的其他成员自己将退出会话。
APP:由应用程序自己定义,解决了RTCP的扩展性问题,并且为协议的实现者提供了很大的灵活性。
3:RTP数据协议
RTP数据协议负责对流媒体数据进行封包并实现媒体流的实时传输,每一个RTP数据报都由头部(Header)和负载(Payload)两个部分组成,其中头部前12个字节的含义是固定的,而负载则可以是音频或者视频数据。
RTP用到的地方就是 PLAY ,服务器往客户端传输数据用UDP协议,RTP是在传输数据的前面加了个12字节的头(描述信息)。
RTP载荷封装设计本文的网络传输是基于IP协议,所以最大传输单元(MTU)最大为1500字节,在使用IP/UDP/RTP的协议层次结构的时候,这 其中包括至少20字节的IP头,8字节的UDP头,以及12字节的RTP头。这样,头信息至少要占用40个字节,那么RTP载荷的最大尺寸为1460字 节。以H264 为例,如果一帧数据大于1460,则需要分片打包,然后到接收端再拆包,组合成一帧数据,进行解码播放。
共同点:
区别:
HTTP: 即超文本传送协议(ftp即文件传输协议)。
HTTP:(Real Time Streaming Protocol),实时流传输协议。
HTTP全称Routing Table Maintenance Protocol(路由选择表维护协议)。
HTTP将所有的数据作为文件做处理。http协议不是流媒体协议。
RTMP协议是Adobe的私有协议,未完全公开,RTSP协议和HTTP协议是共有协议,并有专门机构做维护。
RTMP协议一般传输的是flv,f4v格式流,RTSP协议一般传输的是ts,mp4格式的流。HTTP没有特定的流。
RTSP传输一般需要2-3个通道,命令和数据通道分离,HTTP和RTMP一般在TCP一个通道上传输命令和数据。
1 |
|
1 |
|
GitHub
FFmpeg
ffmpeg 源代码简单分析
simplest_ffmpeg_player
该播放器虽然简单,但是几乎包含了使用FFMPEG播放一个视频所有必备的API,并且使用SDL显示解码出来的视频。
并且支持流媒体等多种视频输入,处于简单考虑,没有音频部分,同时视频播放采用直接延时40ms的方式
对比SDL1.2的流程图,发现变化还是很大的。几乎所有的API都发生了变化。但是函数和变量有一定的对应关系:
SDL_SetVideoMode()————SDL_CreateWindow()
SDL_Surface————SDL_Window
SDL_CreateYUVOverlay()————SDL_CreateTexture()
SDL_Overlay————SDL_Texture
简单解释各个变量的作用:
它们的关系如下图所示:
下图举了个例子,指定了4个SDL_Rect,可以实现4分屏的显示。
1 |
|
标准版的基础之上引入了 SDL 的 Event。
效果如下:
1 |
|
通过 av_read_packet()
,读取一个包,需要说明的是此函数必须是包含整数帧的,不存在半帧的情况。
以 ts 流为例,是读取一个完整的 PES 包(一个完整 pes 包包含若干视频或音频 es 包),读取完毕后,通过 av_parser_parse2()
分析出视频一帧(或音频若干帧),返回,下次进入循环的时候,如果上次的数据没有完全取完,则 st = s->cur_st
; 不会是NULL,即再此进入 av_parser_parse2()
流程,而不是下面的 av_read_packet()
流程.
这样就保证了,如果读取一次包含了 N 帧视频数据(以视频为例),则调用 av_read_frame()
N 次都不会去读数据,而是返回第一次读取的数据,直到全部解析完毕。
函数调用结构图:
av_register_all()
- ffmpeg注册复用器,编码器该函数在所有基于ffmpeg的应用程序中几乎都是第一个被调用的。只有调用了该函数,才能使用复用器,编码器等。
1 | // 可见解复用器注册都是用 |
看一下宏的定义,这里以解复用器为例:
1 |
|
我们以 REGISTER_DEMUXER (AAC, aac)
为例,则它等效于
1 | extern AVInputFormat ff_aac_demuxer; |
从上面这段代码我们可以看出,真正注册的函数是 av_register_input_format(&ff_aac_demuxer)
,那我就看看这个和函数的作用,查看一下 av_register_input_format()
的代码:
1 | void av_register_input_format(AVInputFormat *format) |
这段代码是比较容易理解的,首先先提一点,first_iformat 是个什么东东呢?其实它是 Input Format 链表的头部地址,是一个全局静态变量,定义如下:
1 | /** head of registered input format linked list */ |
由此我们可以分析出 av_register_input_format()
的含义,一句话概括就是:
至此 REGISTER_DEMUXER (X, x)
分析完毕。
同理,复用器道理是一样的,只是注册函数改为 av_register_output_format()
;
既有解复用器又有复用器的话,有一个宏定义:
1 |
可见是分别注册了复用器和解复用器。
此外还有网络协议的注册,注册函数为 ffurl_register_protocol()
,在此不再详述。
1 |
|
整个代码没太多可说的,首先确定是不是已经初始化过了(initialized),如果没有,就调用 avcodec_register_all()
注册编解码器(这个先不分析),然后就是注册,注册,注册…直到完成所有注册。
PS:曾经研究过一阵子 RTMP 协议,以及对应的开源工程 librtmp。在这里发现有一点值得注意,ffmpeg自带了 RTMP 协议的支持,只有使用 rtmpt://, rtmpe://, rtmpte://
等的时候才会使用 librtmp 库。
函数调用关系图如下图所示。av_register_all()
调用了 avcodec_register_all()
。 avcodec_register_all()
注册了和编解码器有关的组件:硬件加速器,解码器,编码器,Parser,Bitstream Filter。av_register_all()
除了调用 avcodec_register_all()
之外,还注册了复用器,解复用器,协议处理器。
下面附上复用器,解复用器,协议处理器的代码。
注册复用器的函数是 av_register_output_format()
。
1 | void av_register_output_format(AVOutputFormat *format) |
注册解复用器的函数是 av_register_input_format()
。
1 | void av_register_input_format(AVInputFormat *format) |
注册协议处理器的函数是 ffurl_register_protocol()
。
1 | int ffurl_register_protocol(URLProtocol *protocol) |
ffmpeg注册编解码器等的函数 avcodec_register_all()
(注意不是 av_register_all()
,那是注册所有东西的)。该函数在所有基于ffmpeg的应用程序中几乎都是第一个被调用的。只有调用了该函数,才能使用编解码器等。
其实注册编解码器和注册复用器解复用器道理是差不多的,重复的内容不再多说。
1 | // 编码器的注册是: |
我们来看一下宏的定义,这里以编解码器为例:
1 |
|
在这里,我发现其实编码器和解码器用的注册函数都是一样的:avcodec_register()
以 REGISTER_DECODER (H264, h264)
为例,就是等效于
1 | extern AVCodec ff_h264_decoder; |
下面看一下 avcodec_register()
的源代码:
1 | //注册所有的AVCodec |
这段代码是比较容易理解的。首先先提一点,first_avcdec 是就是 AVCodec 链表的头部地址,是一个全局静态变量,定义如下:
1 | /* encoder management */ |
由此我们可以分析出avcodec_register()的含义,一句话概括就是:遍历链表并把当前的AVCodec加到链表的尾部。
同理,Parser,BSF(bitstream filters,比特流滤镜),HWACCEL(hardware accelerators,硬件加速器)的注册方式都是类似的。不再详述。
1 |
|
整个代码的过程就是首先确定是不是已经初始化过了(initialized),如果没有,就注册,注册,注册…直到完成所有注册。
函数的调用关系图如下图所示。av_register_all()
调用了 avcodec_register_all()
。因此如果调用过 av_register_all()
的话就不需要再调用 avcodec_register_all()
了。
下面附上硬件加速器,编码器/解码器,parser,Bitstream Filter的注册代码。
硬件加速器注册函数是 av_register_hwaccel()
。
1 | void av_register_hwaccel(AVHWAccel *hwaccel) |
编解码器注册函数是 avcodec_register()
。
1 | av_cold void avcodec_register(AVCodec *codec) |
parser注册函数是 av_register_codec_parser()
。
1 | void av_register_codec_parser(AVCodecParser *parser) |
Bitstream Filter注册函数是 av_register_bitstream_filter()
。
1 | void av_register_bitstream_filter(AVBitStreamFilter *bsf) |
后两个函数中的 avpriv_atomic_ptr_cas()
定义如下。
1 | void *avpriv_atomic_ptr_cas(void * volatile *ptr, void *oldval, void *newval) |
FFMPEG 是特别强大的专门用于处理音视频的开源库。你既可以使用它的 API 对音视频进行处理,也可以使用它提供的工具,如 ffmpeg, ffplay, ffprobe,来编辑你的音视频文件。
本文将简要介绍一下 FFMPEG 库的基本目录结构及其功能,然后详细介绍一下我们在日常工作中,如何使用 ffmpeg 提供的工具来处理音视频文件。
在讲解 FFMPEG 命令之前,我们先要介绍一些音视频格式的基要概念。
音/视频流
在音视频领域,我们把一路音/视频称为一路流。如我们小时候经常使用VCD看港片,在里边可以选择粤语或国语声音,其实就是CD视频文件中存放了两路音频流,用户可以选择其中一路进行播放。
容器
我们一般把 MP4、 FLV、MOV 等文件格式称之为容器。也就是在这些常用格式文件中,可以存放多路音视频文件。以 MP4 为例,就可以存放一路视频流,多路音频流,多路字幕流。
channel
channel 是音频中的概念,称之为声道。在一路音频流中,可以有单声道,双声道或立体声。
我们按使用目的可以将 FFMPEG 命令分成以下几类:
除了 FFMPEG 的基本信息查询命令外,其它命令都按下图所示的流程处理音视频。
然后将编码的数据包传送给解码器(除非为数据流选择了流拷贝,请参阅进一步描述)。 解码器产生未压缩的帧(原始视频/ PCM音频/ …),可以通过滤波进一步处理(见下一节)。 在过滤之后,帧被传递到编码器,编码器并输出编码的数据包。 最后,这些传递给复用器,将编码的数据包写入输出文件。
默认情况下,ffmpeg只包含输入文件中每种类型(视频,音频,字幕)的一个流,并将其添加到每个输出文件中。 它根据以下标准挑选每一个的“最佳”:对于视频,它是具有最高分辨率的流,对于音频,它是具有最多channel的流,对于字幕,是第一个字幕流。 在相同类型的几个流相等的情况下,选择具有最低索引的流。
您可以通过使用 -vn / -an / -sn / -dn
选项来禁用某些默认设置。 要进行全面的手动控制,请使用 -map
选项,该选项禁用刚描述的默认设置。
下面我们就来详细介绍一下这些命令。
FFMPEG 可以使用下面的参数进行基本信息查询。例如,想查询一下现在使用的 FFMPEG 都支持哪些 filter,就可以用 ffmpeg -filters
来查询。详细参数说明如下:
参数 | 说明 |
---|---|
-version | 显示版本。 |
-formats | 显示可用的格式(包括设备)。 |
-demuxers | 显示可用的demuxers。 |
-muxers | 显示可用的muxers。 |
-devices | 显示可用的设备。 |
-codecs | 显示libavcodec已知的所有编解码器。 |
-decoders | 显示可用的解码器。 |
-encoders | 显示所有可用的编码器。 |
-bsfs | 显示可用的比特流filter。 |
-protocols | 显示可用的协议。 |
-filters | 显示可用的libavfilter过滤器。 |
-pix_fmts | 显示可用的像素格式。 |
-sample_fmts | 显示可用的采样格式。 |
-layouts | 显示channel名称和标准channel布局。 |
-colors | 显示识别的颜色名称。 |
接下来介绍的是 FFMPEG 处理音视频时使用的命令格式与参数。
下面是 FFMPEG 的基本命令格式:
1 | ffmpeg [global_options] {[input_file_options] -i input_url} ... |
ffmpeg 通过 -i
选项读取输任意数量的输入“文件”(可以是常规文件,管道,网络流,抓取设备等),并写入任意数量的输出“文件”。
原则上,每个输入 / 输出“文件”都可以包含任意数量的不同类型的视频流(视频 / 音频 / 字幕 / 附件 / 数据)。 流的数量和 / 或类型是由容器格式来限制。 选择从哪个输入进入到哪个输出将自动完成或使用 -map
选项。
要引用选项中的输入文件,您必须使用它们的索引(从 0 开始)。 例如。 第一个输入文件是0,第二个输入文件是1,等等。类似地,文件内的流被它们的索引引用。 例如: 2:3 是指第三个输入文件中的第四个流。
上面就是 FFMPEG 处理音视频的常用命令,下面是一些常用参数:
参数 | 说明 |
---|---|
-f fmt(输入/输出) | 强制输入或输出文件格式。 格式通常是自动检测输入文件,并从输出文件的文件扩展名中猜测出来,所以在大多数情况下这个选项是不需要的。 |
-i url(输入) | 输入文件的网址 |
-y(全局参数) | 覆盖输出文件而不询问。 |
-n(全局参数) | 不要覆盖输出文件,如果指定的输出文件已经存在,请立即退出。 |
-c [:stream_specifier] codec(输入/输出,每个流) | 选择一个编码器(当在输出文件之前使用)或解码器(当在输入文件之前使用时)用于一个或多个流。codec 是解码器/编码器的名称或 copy(仅输出)以指示该流不被重新编码。如:ffmpeg -i INPUT -map 0 -c:v libx264 -c:a copy OUTPUT |
-codec [:stream_specifier]编解码器(输入/输出,每个流) | 同 -c |
-t duration(输入/输出) | 当用作输入选项(在-i之前)时,限制从输入文件读取的数据的持续时间。当用作输出选项时(在输出url之前),在持续时间到达持续时间之后停止输出。 |
-ss位置(输入/输出) | 当用作输入选项时(在-i之前),在这个输入文件中寻找位置。 请注意,在大多数格式中,不可能精确搜索,因此ffmpeg将在位置之前寻找最近的搜索点。 当转码和-accurate_seek被启用时(默认),搜索点和位置之间的这个额外的分段将被解码和丢弃。 当进行流式复制或使用-noaccurate_seek时,它将被保留。当用作输出选项(在输出url之前)时,解码但丢弃输入,直到时间戳到达位置。 |
-frames [:stream_specifier] framecount(output,per-stream) | 停止在帧计数帧之后写入流。 |
-filter [:stream_specifier] filtergraph(output,per-stream) | 创建由filtergraph指定的过滤器图,并使用它来过滤流。filtergraph是应用于流的filtergraph的描述,并且必须具有相同类型的流的单个输入和单个输出。在过滤器图形中,输入与标签中的标签相关联,标签中的输出与标签相关联。有关filtergraph语法的更多信息,请参阅ffmpeg-filters手册。 |
参数 | 说明 |
---|---|
-vframes num(输出) | 设置要输出的视频帧的数量。对于-frames:v,这是一个过时的别名,您应该使用它。 |
-r [:stream_specifier] fps(输入/输出,每个流) | 设置帧率(Hz值,分数或缩写)。作为输入选项,忽略存储在文件中的任何时间戳,根据速率生成新的时间戳。这与用于-framerate选项不同(它在FFmpeg的旧版本中使用的是相同的)。如果有疑问,请使用-framerate而不是输入选项-r。作为输出选项,复制或丢弃输入帧以实现恒定输出帧频fps。 |
-s [:stream_specifier]大小(输入/输出,每个流) | 设置窗口大小。作为输入选项,这是video_size专用选项的快捷方式,由某些分帧器识别,其帧尺寸未被存储在文件中。作为输出选项,这会将缩放视频过滤器插入到相应过滤器图形的末尾。请直接使用比例过滤器将其插入到开头或其他地方。格式是’wxh’(默认 - 与源相同)。 |
-aspect [:stream_specifier] 宽高比(输出,每个流) | 设置方面指定的视频显示宽高比。aspect可以是浮点数字符串,也可以是num:den形式的字符串,其中num和den是宽高比的分子和分母。例如“4:3”,“16:9”,“1.3333”和“1.7777”是有效的参数值。如果与-vcodec副本一起使用,则会影响存储在容器级别的宽高比,但不会影响存储在编码帧中的宽高比(如果存在)。 |
-vn(输出) | 禁用视频录制。 |
-vcodec编解码器(输出) | 设置视频编解码器。这是 -codec:v 的别名。 |
-vf filtergraph(输出) | 创建由filtergraph指定的过滤器图,并使用它来过滤流。 |
参数 | 说明 |
---|---|
-aframes(输出) | 设置要输出的音频帧的数量。这是 -frames:a 的一个过时的别名。 |
-ar [:stream_specifier] freq(输入/输出,每个流) | 设置音频采样频率。对于输出流,它默认设置为相应输入流的频率。对于输入流,此选项仅适用于音频捕获设备和原始分路器,并映射到相应的分路器选件。 |
-ac [:stream_specifier]通道(输入/输出,每个流) | 设置音频通道的数量。对于输出流,它默认设置为输入音频通道的数量。对于输入流,此选项仅适用于音频捕获设备和原始分路器,并映射到相应的分路器选件。 |
-an(输出) | 禁用录音。 |
-acodec编解码器(输入/输出) | 设置音频编解码器。这是-codec的别名:a。 |
-sample_fmt [:stream_specifier] sample_fmt(输出,每个流) | 设置音频采样格式。使用-sample_fmts获取支持的样本格式列表。 |
-af filtergraph(输出) | 创建由filtergraph指定的过滤器图,并使用它来过滤流。 |
了解了这些基本信息后,接下来我们看看 FFMPEG 具体都能干些什么吧。
首先通过下面的命令查看一下 mac 上都有哪些设备。
1 | ffmpeg -f avfoundation -list_devices true -i "" |
录屏
1 | ffmpeg -f avfoundation -i 1 -r 30 out.yuv |
-f 指定使用 avfoundation 采集数据。
-i 指定从哪儿采集数据,它是一个文件索引号。在我的MAC上,1代表桌面(可以通过上面的命令查询设备索引号)。
-r 指定帧率。按ffmpeg官方文档说-r与-framerate作用相同,但实际测试时发现不同。-framerate 用于限制输入,而 -r 用于限制输出。
注意:桌面的输入对帧率没有要求,所以不用限制桌面的帧率。其实限制了也没用。
录屏+声音
1 | ffmpeg -f avfoundation -i 1:0 -r 29.97 -c:v libx264 -crf 0 -c:a libfdk_aac -profile:a aac_he_v2 -b:a 32k out.flv |
-i 1:0 冒号前面的 “1” 代表的屏幕索引号。冒号后面的”0”代表的声音索相号。
-c:v 与参数 -vcodec 一样,表示视频编码器。c 是 codec 的缩写,v 是video的缩写。
-crf 是 x264 的参数。 0 表式无损压缩。
-c:a 与参数 -acodec 一样,表示音频编码器。
-profile 是 fdk_aac 的参数。 aac_he_v2 表式使用 AAC_HE v2 压缩数据。
-b:a 指定音频码率。 b 是 bitrate的缩写, a是 audio的缩与。
录视频
1 | ffmpeg -framerate 30 -f avfoundation -i 0 out.mp4 |
-framerate 限制视频的采集帧率。这个必须要根据提示要求进行设置,如果不设置就会报错。
-f 指定使用 avfoundation 采集数据。
-i 指定视频设备的索引号。
视频+音频
1 | ffmpeg -framerate 30 -f avfoundation -i 0:0 out.mp4 |
录音
1 | ffmpeg -f avfoundation -i :0 out.wav |
录制音频裸数据
1 | ffmpeg -f avfoundation -i :0 -ar 44100 -f s16le out.pcm |
流拷贝是通过将 copy 参数提供给-codec选项来选择流的模式。它使得ffmpeg省略了指定流的解码和编码步骤,所以它只能进行多路分解和多路复用。 这对于更改容器格式或修改容器级元数据很有用。 在这种情况下,上图将简化为:
由于没有解码或编码,速度非常快,没有质量损失。 但是,由于许多因素,在某些情况下可能无法正常工作。 应用过滤器显然也是不可能的,因为过滤器处理未压缩的数据。
抽取音频流
1 | ffmpeg -i input.mp4 -acodec copy -vn out.aac |
acodec: 指定音频编码器,copy 指明只拷贝,不做编解码。
vn: v 代表视频,n 代表 no 也就是无视频的意思。
抽取视频流
1 | ffmpeg -i input.mp4 -vcodec copy -an out.h264 |
vcodec: 指定视频编码器,copy 指明只拷贝,不做编解码。
an: a 代表视频,n 代表 no 也就是无音频的意思。
转格式
1 | ffmpeg -i out.mp4 -vcodec copy -acodec copy out.flv |
上面的命令表式的是音频、视频都直接 copy,只是将 mp4 的封装格式转成了 flv。
音视频合并
1 | ffmpeg -i out.h264 -i out.aac -vcodec copy -acodec copy out.mp4 |
提取YUV数据
1 | ffmpeg -i input.mp4 -an -c:v rawvideo -pixel_format yuv420p out.yuv |
-c:v rawvideo 指定将视频转成原始数据
-pixel_format yuv420p 指定转换格式为 yuv420p
YUV 转 H264
1 | ffmpeg -f rawvideo -pix_fmt yuv420p -s 320x240 -r 30 -i out.yuv -c:v libx264 -f rawvideo out.h264 |
提取 PCM 数据
1 | ffmpeg -i out.mp4 -vn -ar 44100 -ac 2 -f s16le out.pcm |
PCM 转 WAV
1 | ffmpeg -f s16be -ar 8000 -ac 2 -acodec pcm_s16be -i input.raw output.wav |
在编码之前,ffmpeg 可以使用 libavfilter 库中的过滤器处理原始音频和视频帧。 几个链式过滤器形成一个过滤器图形。 ffmpeg 区分两种类型的过滤器图形:简单和复杂。
简单的过滤器图是那些只有一个输入和输出,都是相同的类型。 在上面的图中,它们可以通过在解码和编码之间插入一个额外的步骤来表示:
简单的 filtergraphs 配置了 per-stream-filter 选项(分别为视频和音频使用 -vf
和 -af
别名)。 一个简单的视频 filtergraph 可以看起来像这样的例子:
请注意,某些滤镜会更改帧属性,但不会改变帧内容。 例如。 上例中的 fps 过滤器会改变帧数,但不会触及帧内容。 另一个例子是 setpts 过滤器,它只设置时间戳,否则不改变帧。
复杂的过滤器图是那些不能简单描述为应用于一个流的线性处理链的过滤器图。 例如,当图形有多个输入和/或输出,或者当输出流类型与输入不同时,就是这种情况。 他们可以用下图来表示:
复杂的过滤器图使用 -filter_complex
选项进行配置。 请注意,此选项是全局性的,因为复杂的过滤器图形本质上不能与单个流或文件明确关联。
-lavfi
选项等同于 -filter_complex
。
一个复杂的过滤器图的一个简单的例子是覆盖过滤器,它有两个视频输入和一个视频输出,包含一个视频叠加在另一个上面。 它的音频对应是 amix 滤波器。
添加水印
1 | ffmpeg -i out.mp4 -vf "movie=logo.png,scale=64:48[watermask];[in][watermask] overlay=30:10 [out]" water.mp4 |
删除水印
先通过 ffplay 找到要删除 LOGO 的位置
1 | ffplay -i test.flv -vf delogo=x=806:y=20:w=70:h=80:show=1 |
使用 delogo 滤镜删除 LOGO
1 | ffmpeg -i test.flv -vf delogo=x=806:y=20:w=70:h=80 output.flv |
视频缩小一倍
1 | ffmpeg -i out.mp4 -vf scale=iw/2:-1 scale.mp4 |
iw/2:-1
中的 iw 指定按整型取视频的宽度。 -1 表示高度随宽度一起变化。视频裁剪
1 | ffmpeg -i VR.mov -vf crop=in_w-200:in_h-200 -c:v libx264 -c:a copy -video_size 1280x720 vr_new.mp4 |
crop 格式:crop=out_w:out_h:x:y
out_w: 输出的宽度。可以使用 in_w 表式输入视频的宽度。
out_h: 输出的高度。可以使用 in_h 表式输入视频的高度。
x : X坐标
y : Y坐标
如果 x 和 y 设置为 0, 说明从左上角开始裁剪。如果不写是从中心点裁剪。
倍速播放
1 | ffmpeg -i out.mp4 -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]" -map "[v]" -map "[a]" speed2.0.mp4 |
-filter_complex 复杂滤镜,[0:v]
表示第一个(文件索引号是 0)文件的视频作为输入。setpts=0.5*PTS
表示每帧视频的 pts 时间戳都乘 0.5 ,也就是差少一半。[v]
表示输出的别名。音频同理就不详述了。
map 可用于处理复杂输出,如可以将指定的多路流输出到一个输出文件,也可以指定输出到多个文件。”[v]” 复杂滤镜输出的别名作为输出文件的一路流。上面 map的用法是将复杂滤镜输出的视频和音频输出到指定文件中。
对称视频
1 | ffmpeg -i out.mp4 -filter_complex "[0:v]pad=w=2*iw[a];[0:v]hflip[b];[a][b]overlay=x=w" duicheng.mp4 |
如果要修改为垂直翻转可以用 vflip。
画中画
1 | ffmpeg -i out.mp4 -i out1.mp4 -filter_complex "[1:v]scale=w=176:h=144:force_original_aspect_ratio=decrease[ckout];[0:v][ckout]overlay=x=W-w-10:y=0[out]" -map "[out]" -movflags faststart new.mp4 |
录制画中画
1 | ffmpeg -f avfoundation -i "1" -framerate 30 -f avfoundation -i "0:0" |
多路视频拼接
1 | ffmpeg -f avfoundation -i "1" -framerate 30 -f avfoundation -i "0:0" -r 30 -c:v libx264 -preset ultrafast -c:a libfdk_aac -profile:a aac_he_v2 -ar 44100 -ac 2 -filter_complex "[0:v]scale=320:240[a];[a]pad=640:240[b];[b][1:v]overlay=320:0[out]" -map "[out]" -movflags faststart -map 1:a c.mp4 |
裁剪
1 | ffmpeg -i out.mp4 -ss 00:00:00 -t 10 out1.mp4 |
-ss 指定裁剪的开始时间,精确到秒
-t 被裁剪后的时长。
合并
首先创建一个 inputs.txt 文件,文件内容如下:
1 | file '1.flv' |
然后执行下面的命令:
1 | ffmpeg -f concat -i inputs.txt -c copy output.flv |
hls切片
1 | ffmpeg -i out.mp4 -c:v libx264 -c:a libfdk_aac -strict -2 -f hls out.m3u8 |
-strict -2 指明音频使有AAC。
-f hls 转成 m3u8 格式。
视频转 JPEG
1 | ffmpeg -i test.flv -r 1 -f image2 image-%3d.jpeg |
视频转 gif
1 | ffmpeg -i out.mp4 -ss 00:00:00 -t 10 out.gif |
图片转视频
1 | ffmpeg -f image2 -i image-%3d.jpeg images.mp4 |
推流
1 | ffmpeg -re -i out.mp4 -c copy -f flv rtmp://server/live/streamName |
拉流保存
1 | ffmpeg -i rtmp://server/live/streamName -c copy dump.flv |
转流
1 | ffmpeg -i rtmp://server/live/originalStream -c:a copy -c:v copy -f flv rtmp://server/live/h264Stream |
实时推流
1 | ffmpeg -framerate 15 -f avfoundation -i "1" -s 1280x720 -c:v libx264 -f flv rtmp://localhost:1935/live/room |
播放 YUV 数据
1 | ffplay -pix_fmt nv12 -s 192x144 1.yuv |
播放 YUV 中的 Y 平面
1 | ffplay -pix_fmt nv21 -s 640x480 -vf extractplanes='y' 1.yuv |
本文以文档的形式来描述FFmpeg怎么入门,这也是为以后写文档做的一个大题框架格式。
整理出开源代码 ffmpeg 的资料,方便公司同事后续使用。
较为详细的介绍 ffmpeg 的功能、使用以及二次开发。
希望了解 ffmpeg 知识,从事 USM 及 IPTV 的同事。
TODO
缩略语/术语 | 全 称 | 说 明 |
---|---|---|
ffmpeg | Fast forword mpeg | 音视频转换器 |
ffplay | Fast forword play | 用 ffmpeg 实现的播放器 |
ffserver | Fast forword server | 用 ffmpeg 实现的 rstp 服务器 |
ffprobe | Fast forword probe | 用来输入分析输入流。 |
FFmpeg 是一个开源免费跨平台的视频和音频流方案,属于自由软件,采用 LGPL 或 GPL 许可证(依据你选择的组件)。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库 libavcodec,为了保证高可移植性和编解码质量,libavcodec 里很多 codec 都是从头开发的。
FFmpeg 项目由以下几部分组成:
(1)ffmpeg 视频文件转换命令行工具, 也支持经过实时电视卡抓取和编码成视频文件.
(2)ffserver 基于 HTTP、RTSP 用于实时广播的多媒体服务器. 也支持时间平移
(3)ffplay 用 SDL 和 FFmpeg 库开发的一个简单的媒体播放器
(4)libavcodec 一个包含了所有 FFmpeg 音视频编解码器的库. 为了保证最优性能和高可复用性, 大多数编解码器从头开发的.
(5)libavformat 一个包含了所有的普通音视格式的解析器和产生器的库
将所有源代码压缩在一个文件夹中,例如 /绝对路径/ffmpeg
。
在终端输入以下指令:
1 | cd /绝对路径/ffmpeg |
至此,ffmpeg 安装编译通过,可以进行对音视频的操作。
ffplay 的编译需要依赖于 SDL 库,所以要想编译成功 ffplay,必须先安装 SDL 库,
安装方法:下载最新版本的 SDL 相应版本的 SDL 源码,编译,即可生成 SDL 库。
1 | 首先下载 SDL 软件包 |
1 | -L license |
1 | -b bitrate 设置比特率,缺省 200kb/s |
1 | -g gop_size 设置图像组大小 |
1 | -ab bitrate 设置音频码率 |
1 | -vd device 设置视频捕获设备。比如/dev/video0 |
1 | -map file:stream 设置输入流映射 |
1 | ./ffmpeg -y -i /rootVideoConverter/123.avi -ab 56 -ar 22050 -b 1500 -r 15 -qscale 10 –s 480x350 /root/VideoConverter/234.flv |
ffmpeg 支持的编解码器种类共有 280 多种,涵盖了几乎所有常见音视频编码格式,能解码几乎所有的音视频,每种音视频编解码器的实现都在 libavcodec 目录下有具体的C 语言实现,具体的支持情况参见:
注:编码器和解码器的名称不是完全匹配的,因此有些编码器没有对应相同名称的解码器,反之,
解码器也一样。即使编码和解码都支持也不一定是完全对应的,例如 h263 解码器对应有 h263p 和 h263 编码器。
ffmpeg 支持对绝大多数的容器格式的读写操作,共计 190 多种,涵盖了互联网上各种常见媒体格式及日常生活中及专业应用中的各种媒体格式。详细的支持情况参见:
Filters | 说明 |
---|---|
aformat | Convert the input audio to one of the specified formats. |
anull | Pass the source unchanged to the output. |
aresample | Resample audio data. |
ashowinfo | Show textual information for each audio frame. |
abuffer | Buffer audio frames, and make them accessible to the filterchain. |
anullsrc | Null audio source, never return audio frames. |
abuffersink | Buffer audio frames, and make them available to the end of the filter graph. |
anullsink | Do absolutely nothing with the input audio. |
copy | Copy the input video unchanged to the output. |
crop | Crop the input video to width:height:x:y . |
drawbox | Draw a colored box on the input video. |
fade | Fade in/out input video |
fieldorder | Set the field order. |
fifo | Buffer input images and send them when they are requested. |
format | Convert the input video to one of the specified pixel formats. |
gradfun | Debands video quickly using gradients. |
hflip | Horizontally flip the input video. |
lut | Compute and apply a lookup table to the RGB/YUV input video. |
lutrgb | Compute and apply a lookup table to the RGB input video. |
lutyuv | Compute and apply a lookup table to the YUV input video. |
negate | Negate input video. |
noformat | Force libavfilter not to use any of the specified pixel formats for the input to the next filter. |
null | Pass the source unchanged to the output. |
overlay | Overlay a video source on top of the input. |
pad | Pad input image to width:height[:x:y[:color]] (default x and y: 0, default color: black) . |
pixdesctest | Test pixel format definitions. |
scale | Scale the input video to width:height size and/or convert the image format. |
select | Select frames to pass in output. |
setdar | Set the frame display aspect ratio. |
setpts | Set PTS for the output video frame. |
setsar | Set the pixel sample aspect ratio. |
settb | Set timebase for the output link. |
showinfo | Show textual information for each video frame. |
slicify | Pass the images of input video on to next video filter as multiple slices. |
split | Pass on the input to two outputs. |
transpose | Transpose input video. |
unsharp | Sharpen or blur the input video. |
vflip | Flip the input video vertically. |
buffer | Buffer video frames, and make them accessible to the filterchain. |
color | Provide an uniformly colored input, syntax is: [color[:size[:rate]]] |
movie | Read from a movie source. |
nullsrc | Null video source, never return images. |
rgbtestsrc | Generate RGB test pattern. |
testsrc | Generate test pattern. |
buffersink | Buffer video frames, and make them available to the end of the filter graph. |
nullsink | Do absolutely nothing with the input video. |
ffmpeg 支持常见的图像颜色空间,并且在 libavswcale 中定义了颜色空间转换的相关函数实现各种颜色模式的互转。具体的支持情况见:
一、视频
3gp 177X144
支持播放,在 windows 下播放正常,但是在 linux 下面偶有 BUG 如果发现画面无法显示而声音可以播放的情况下 可以试着切换全屏或者切换分辨率。
1 | AVI 208X176 支持 |
二、音频
1 | AC3 48KHZ 支持 |
三、图像
1 | PNG 支持 |
第一步:准备媒体
前面已经讲的很清楚了,ffmpeg 如何安装不在赘述。准备好相应的文件,如图 2-1所示。
第二步:启动 ffmpeg
由于做的是格式转换,在 ffserver 上不能直观的看见结果,故我是在 linux 下进行的。打开终端,值得一提的是格式转换需要超级用户才能进行,故在命令行输入:su,<回车>
,输入密码进入超级用户,本例中,以 FFmpeg 将 test.avi 转换为 test.mpg。在命令行中输入:
1 | ./ffmpeg –i test.avi –r 25 –s 720x400 test.mpg |
其中原格式分辨率为 320x240,将转为 720x400,-r 前面已经解释其含义,表示设置帧频为 25。转换成功后如图 2-2 所示,前后两种格式播放效果如图 2-3 所示。相应的,转换为其他格式做相应的变化即可。
同时还可以在转换格式时进行强制的音视频转换,如 –vcodec + 格式
,将会强制将视频按指定格式编码,-acodec +格式
,将会强制按指定格式编码音频信息。在转换中有很多其他参数可以指定,如码率、分辨率、帧率等,具体按照 ffmpeg 的参数说明指定参数即可。但有一条转低不转高的原则需要注意,即品质差的音视频转换不建议转换到品质好的音视频。
再说说如何在转换视频的时候将音频合成到视频中,且覆盖其原来的音频。这个现在摸索出两种方法。
方法一:需要两条命令实现,先在命令行中输入:
1 | ./ffmpeg –i test.avi -an –r 25 test.mpg |
此时将生成一个没有声音的 test.mpg 视频,再在命令行中输入:
1 | ./ffmpeg –i test.mpg –i test.mp3 –r 25 test1.mpg |
此时将会生成一个名为 test1.mpg 的视频。该视频播放时视频为 test.avi 的视频,但音频变为了 test.mp3 的音频了。
方法二:只要一条指令即可实现。在命令行中输入:
1 | ./ffmpeg –i test.avi –i test.mp3 –vcodec copy –acodec copy –r 25 test2.mpg |
此时将会生成一个名为 test2.mpg 的视频,播放时其视频为 test.avi 的视频,音频为 test.mp3。–vcodec copy
为 force video codec(‘copy’ to copy stream)。
有一点需要注意,文件命名不能有空格,否则会导致编译时不能通过。另外,-an
为不能使音频记录。
第三步:播放媒体
播放我们转换的媒体,看看是否满足我们当初的愿望,不出什么差错的话,是完全能够满足我们的要求的。
截取一张 300x200
尺寸大小的格式为 jpg 的一张图片:
1 | ./ffmpeg –i test.avi –y –f image2 –t 0.001 –s 300x200 test.jpg |
要截取指定时间的图片,如 5 秒之后的:
1 | ./ffmpeg –i test.avi –y –f image2 –ss 5 –t 0.001 –s 300x200 test.jpg |
其中,-ss
后的单位为秒,也可写成:-ss 00:00:05
。
把视频的前 30 帧转换为一个动态的 gif 图。需要说明的是,转换成功之后,如果用 ffplay 播放是看不出效果的,建议换成其他图片播放器播放。其转换命令为:
1 | ./ffmpeg –i test.avi –vframes 30 –pix_fmt rgb24–y –f gif test.gif |
也可以从视频中的第 10 秒开始截取后面的 5 秒内容转换为一个无限重播的动态 gif 图。其命令为:
1 | ./ffmpeg –i test.avi –pix_fmt rgb24 –ss 10 –t 5 –y –f gif test.gif |
上面两种动态 gif 都是只播一次,想让其一直播,可再加一个参数:-loop_output 0
。
屏幕录制其命令为:
1 | ./ffmpeg -f x11grab -r 25 -s wxga -i :0.0 /tmp/outputFile.mpg |
其他相关参数可自行添加。需要说明的是,各个版本的 ffmpeg 对屏幕录制的命令不一。如果你只想录制一个应用程序窗口或者桌面上的一个固定区域,那么可以指定偏移位置和区域大小。使用 xwininfo -frame
命令可以完成查找上述参数。
注:ffmpeg 的屏幕录制功能只能在 Linux 环境下有效。并且在配置时需要添加 –enable-x11grub
指令,默认关闭。
把摄像头的实时视频录制下来,存储为文件
1 | ./ffmpeg -f video4linux -s 320x240 -r 10 -i /dev/video0 test.asf |
录音,其命令为:
1 | ./ffmpeg –i /dev/dsp -f oss test.mp3 |
一、安装 ffmpeg
在 ubuntu 下,运行 sudo apt-get ffmpeg
安装 ffmpeg,在其他 linux 操作系统下,见 ffmpeg 的编译过程(编译完成后可执行自动安装)。
二、准备预播放的媒体文件
如 test.Mp3,在本文档中,默认放入用户文件夹下得 Music 文件夹内.(直接从设备采集不在本文档叙述范围之内)
三、修改 ffserver 配置信息
ffserver 配置文件为: /etc/ffserver.conf
打开,填写配置信息.配置信息包括三方面:
(1)端口绑定等基本信息,在 ·/etc/ffserver.conf· 中有详细注释,在此不再重复,最终配置信息为:
1 | Port 8090 |
(2)媒体文件配置信息.本信息根据具体的媒体文件类型直接在配置文件中取消注释掉相应文件类型的配置信息,然后填写文件路径即可:
1 | MP3 audio |
四、启动 ffserver
在终端中运行: sudo ffserver -f /etc/ffserver.conf
启动 ffserver.
五、播放流媒体
在浏览器中输入 http://127.0.0.1:8090/test.mp3 即可播放音乐.
在终端中输入 ffplay http://localhost:8090/test.mp3
可播放流媒体.
一、准备媒体
按照上节步骤安装 ffmpeg,保证摄像头和声卡可用,将从摄像头和声卡获取音视频信息。
二、修改 ffserver 配置信息
ffserver 配置文件为: /etc/ffserver.conf
打开,填写配置信息.配置信息包括三方面:
(1)端口绑定等基本信息,在 /etc/ffserver.conf
中有详细注释,在此不再重复,最终配
置信息为:
1 | Port 8090 |
(2)fend(传冲信息),在文件播放中,基本不用动本配置信息,只需要根据具体情况分配缓冲文件.最终配置信息如下:
1 | <Feed feed1.ffm> |
(3)媒体文件配置信息.本信息根据具体的媒体文件类型直接在配置文件中取消注释掉相应文件类型的配置信息,然后填写文件路径即可:
(中间会有很多很多配置信息,都是关于音视频的,有些配置还不懂,慢慢摸索吧)
1 | <Stream test1.mpg> |
三、启动 FFserver
在终端中运行: sudo ffserver -f /etc/ffserver.conf
启动 ffserver.
四、启动 ffmpeg
本例中,以 ffmpeg 作为实时摄像头采集输入.在命令行中输入:
1 | ./ffmpeg -f video4linux2 -r 25 -i /dev/video0 /tmp/feed1.ffm |
如果有音频设备,则采集音频的命令如下:
1 | ./ffmpeg -f oss -i /dev/dsp -f video4linux2 -r 25 -i /dev/video0 /tmp/feed1.ffm |
(音频格式参数自己配置)
五、播放流媒体
在浏览器中输入 http://127.0.0.1:8090/test1.mpg 即可播放音乐.
在终端中输入 ffplay http://localhost:8090/test.swf
可播放流媒体.
目录 | 文件 | 简要说明 |
libavformat 主要存放ffmpeg 支持的各种编解码 器的实现及ffmpeg 编解码功能相关的 数据结构定义及函 数定义和声明 | allcodecs.c | 简单的注册类函数 |
avcodec.h | 编解码相关结构体定义和函数原型声明 | |
dsputil.c | 限幅数组初始化 | |
dsputil.h | 限幅数组声明 | |
imgconvert.c | 颜色空间转换相关函数实现 | |
imgconvert_template.h | 颜色空间转换相关结构体定义和函数声明 | |
utils_codec.c | 一些解码相关的工具类函数的实现 | |
mpeg4audio.c | mpeg4 音频编解码器的函数实现 | |
mpeg4audio.h | mpeg4 音频编解码器的函数声明 | |
mpeg4data.h | mpeg4 音视频编解码器的公用的函数声明及数据结构定义 | |
mpeg4video.c | mpeg4 视频编解码器的函数实现 | |
mpeg4video.h | mpeg4 视频编解码器的函数的声明及先关数据结构的定义 | |
mpeg4videodec.c | mpeg4 视频解码器的函数实现 | |
mpeg4videoenc.c | mpeg4 视频编码器的函数实现 | |
libavformat 主要存放ffmpeg支 持的各种媒体格式 MUXER/DEMUXER 和数据流协议的定 义和实现文件以及 ffmpeg解复用相 关的数据结构及 函数定 | allformats.c | 简单注册类函数 |
avformat.h | 文件和媒体格式相关函数声明和数据结构定义 | |
avio.c | 无缓冲 IO 相关函数实现 | |
avio.h | 无缓冲 IO 相关结构定义和函数声明 | |
aviobuf.c | 有缓冲数据 IO 相关函数实现 | |
cutils.c | 简单的字符串操作函数 | |
utils_format.c | 文件和媒体格式相关的工具函数的实现 | |
file.c | 文件 io 相关函数 | |
...... | 其他相关媒体流 IO 的函数和数据结构实现文件。如:rtsp、http 等。 | |
avi.c | AVI 格式的相关函数定西 | |
avi.h | AVI 格式的相关函数声明及数据结构定义 | |
avidec.c | AVI 格式 DEMUXER 相关函数定义 | |
avienc.c | AVI 格式 MUXER 相关函数定义 | |
...... | 其他媒体格式的 muxer/demuxer 相关函数及数据结构定义和声明文件 | |
libavutil 主要存放ffmpeg 工具类函数的定义 | avutil.h | 简单的像素格式宏定义 |
bswap.h | 简单的大小端转换函数的实现 | |
commom.h | 公共的宏定义和简单函数的实现 | |
mathematics.c | 数学运算函数实现 | |
rational.h | 分数相关表示的函数实现 |
ffmpeg 项目的数据 IO 部分主要是在 libavformat 库中实现,某些对于内存的操作部分在 libavutil 库中。数据 IO 是基于文件格式(Format)以及文件传输协议(Protocol)的,与具体的编解码标准无关。
ffmpeg 工程转码时数据 IO 层次关系如图所示:
对于上面的数据 IO 流程,具体可以用下面的例子来说明,我们从一个 http 服务器获取音视频数据,格式是 flv 的,需要通过转码后变成 avi 格式,然后通过 udp 协议进行发布。其过程就如下所示:
在 libavformat 库中与数据 IO 相关的数据结构主要有 URLProtocol、URLContext、ByteIOContext、AVFormatContext 等,各结构之间的关系如图所示。
1、URLProtocol 结构
表示广义的输入文件,该结构体提供了很多的功能函数,每一种广义的输入文件(如:file、pipe、tcp、rtp 等等)对应着一个 URLProtocol
结构,在 av_register_all()
中将该结构体初始化为一个链表,表头为 avio.c
里的 URLProtocol *first_protocol = NULL;
保存所有支持的输入文件协议,该结构体的定义如下:
1 | typedef struct URLProtocol |
注意到,URLProtocol
是一个链表结构,这是为了协议的统一管理,ffmpeg 项目中将所有的用到的协议都存放在一个全局变量 first_protocol 中,协议的注册是在 av_register_all
中完成的,新添加单个协议可以调用 av_register_protocol2
函数实现。而协议的注册就是将具体的协议对象添加至 first_protocol
链表的末尾。
URLProtocol
在各个具体的文件协议中有一个具体的实例,如在 file 协议中定义为:
1 | URLProtocol ff_file_protocol = { |
2、URLContext 结构
URLContext 提供了与当前打开的具体的文件协议(URL)相关数据的描述,在该结构中定义了指定当前 URL(即 filename 项)所要用到的具体的 URLProtocol,即:提供了一个在 URLprotocol 链表中找到具体项的依据,此外还有一些其它的标志性的信息,如 flags, is_streamed 等。它可以看成某一种协议的载体。其结构定义如下:
1 | typedef struct URLContext |
那么 ffmpeg 依据什么信息初始化 URLContext?然后又是如何初始化 URLContext的呢?
在打开一个 URL 时,全局函数 ffurl_open 会根据 filename 的前缀信息来确定 URL所使用的具体协议,并为该协议分配好资源,再调用 ffurl_connect 函数打开具体协议,即调用协议的 url_open,调用关系如下:
1 | int av_open_input_file(AVFormatContext **ic_ptr, const char *filename, |
浅蓝色部分的函数完成了 URLContext 函数的初始化,URLContext 使 ffmpeg 外所暴露的接口是统一的,而不是对于不同的协议用不同的函数,这也是面向对象思维的体现。在此结构中还有一个值得说的是 priv_data 项,这是结构的一个可扩展项,具体协议可以根据需要添加相应的结构,将指针保存在这就行。
3、AVIOContext 结构
AVIOContext(即:ByteIOContext)是由 URLProtocol 和 URLContext 结构扩展而来,也是 ffmpeg 提供给用户的接口,它将以上两种不带缓冲的读取文件抽象为带缓冲的读取和写入,为用户提供带缓冲的读取和写入操作。数据结构定义如下:
1 | typedef struct |
结构简单的为用户提供读写容易实现的四个操作,read_packet write_packet read_pause read_seek,极大的方便了文件的读取,四个函数在加了缓冲机制后被中转到,URLContext 指向的实际的文件协议读写函数中。
下面给出 0.8 版本中是如何将 AVIOContext 的读写操作中转到实际文件中的。
在 avio_open()函数中调用了 ffio_fdopen()函数完成了对 AVIOContex 的初始化,其调用过程如下:
1 | int avio_open(AVIOContext **s, const char *filename, int flags) |
函数调用完成了对 AVIOContext 的初始化,在初始化的过程中,将AVIOContext 的 read_packet 、 write_packet 、 seek 分别初始化为: ffurl_read ffurl_write ffurl_seek , 而这三个函数又将具体的读写操作中转为:
h->prot->url_read、h->prot->url_write、h->prot->url_seek
,另外两个变量初始化时也被相应的中转,如下:
1 | (*s)->read_pause = (int (*)(void *, int))h->prot->url_read_pause; |
所以,可以简要的描述为:AVIOContext 的接口口是加了缓冲后的 URLProtocol 的函数接口。
在 aviobuf.c 中定义了一系列关于 ByteIOContext 这个结构体的函数,如下
put_xxx 系列:
1 | void put_byte(ByteIOContext *s, int b); |
get_xxx 系列:
1 | int get_buffer(ByteIOContext *s, unsigned char *buf, int size); |
这些 put_xxx 及 get_xxx 函数是用于从缓冲区 buffer 中写入或者读取若干个字节,对于读写整型数据,分别实现了大端和小端字节序的版本。而缓冲区 buffer 中的数据又是从何而来呢,有一个 fill_buffer 的函数,在 fill_buffer 函数中调用了ByteIOContext 结构的 read_packet 接口。在调用 put_xxx 函数时,并没有直接进行真
正写入操作,而是先缓存起来,直到缓存达到最大限制或调用 flush_buffer 函数对缓冲区进行刷新,才使用 write_packet 函数进行写入操作。
ffmpeg 的 demuxer 和 muxer 接口分别在 AVInputFormat 和 AVOutputFormat 两个结构体中实现,在 av_register_all()函数中将两个结构分别静态初始化为两个链表,保存在全局变量:first_iformat 和 first_oformat 两个变量中。在 FFmpeg 的文件转换或者打开过程中,首先要做的就是根据传入文件和传出文件的后缀名匹配合适的 demuxer和 muxer,得到合适的信息后保存在 AVFormatContext 中。
1、AVInputFormat
该结构被称为 demuxer,是音视频文件的一个解封装器,它的定义如下:
1 | typedef struct AVInputFormat |
对于不同的文件格式要实现相应的函数接口,这样每一种格式都有一个对应的demuxer,所有的 demuxer 都保存在全局变量 first_iformat 中。红色表示提供的接口。
2、AVOutputFormat
该结构与 AVInputFormat 类似也是在编译时静态初始化,组织为一个链表结构,提供了多个 muxer 的函数接口。
1 | int (*write_header)(struct AVFormatContext *); |
对于不同的文件格式要实现相应的函数接口,这样每一种格式都有一个对应的 muxer,所有的 muxer 都保存在全局变量 first_oformat 中。
3、AVFormatContext
该结构表示与程序当前运行的文件容器格式使用的上下文,着重于所有文件容器共有的属性,在运行时动态的确定其值,是 AVInputFormat 和 AVOutputFormat 的载体,但同一个结构对象只能使 AVInputFormat 和 AVOutputFormat 中的某一个有效。每一个输入和输出文件,都在
1 | static AVFormatContext *output_files[MAX_FILES] 和 |
定义的指针数组全局变量中有对应的实体。对于输入和输出,因为共用的是同一个结构体,所以需要分别对该结构中如下定义的 iformat 或 oformat 成员赋值。在转码时读写数据是通过 AVFormatContext 结构进行的。定义如下:
1 | typedef struct AVFormatContext |
注释部分的成员是 AVFormatContext 中最为重要的成员变量,这些变量的初始化是ffmpeg 能正常工作的必要条件,那么,AVFormatContext 是如何被初始化的呢?文件的格式是如何被探测到的呢?
首先我们来探讨:
1 | struct AVInputFormat *iformat; //指向具体的 demuxer |
三个成员的初始化。
在 avformat_open_input() 函数中调用了 init_input() 函数,然后用调用了av_probe_input_format()函数实现了对 AVFormatContext 的初始化。其调用关系如下:
1 | int av_open_input_file(AVFormatContext **ic_ptr, const char *filename, |
函数用途是根据传入的 probe data 数据,依次调用每个 demuxer 的 read_probe 接口,来进行该 demuxer 是否和传入的文件内容匹配的判断。与 demuxer 的匹配不同,muxer的匹配是调用 guess_format 函数,根据 main( ) 函数的 argv 里的输出文件后缀名来进行的。至此完成了前三个重要成员的初始化,具体的做法就不在深入分析。
下面分别给出 av_read_frame 函数以及 av_write_frame 函数的基本流程。
1 | int av_read_frame(AVFormatContext *s, AVPacket *pkt); |
由上可见,对 AVFormatContext 的读写操作最终是通过 ByteIOContext 来实现的,这样,AVFormatContext 与 URLContext 就由 ByteIOContext 结构联系到一起了。在AVFormat 结构体中有一个 packet 的缓冲区 raw_packet_buffer,是 AVPackList 的指针类型,av_read_packet 函数将读到的包添加至 raw_packet_buffer 链表末尾。
编解码模块主要包含的数据结构为:AVCodec、AVCodecContext 每一个解码类型都会有自己的 Codec 静态对像,Codec 的 int priv_data_size 记录该解码器上下文的结构大小,如 MsrleContext 。这些都是编译时确定的,程序运行时通过avcodec_register_all()将所有的解码器注册成一个链表。在 av_open_input_stream()函数中调用 AVInputFormat 的 read_header()中读文件头信息时,会读出数据流的CodecID,即确定了他的解码器 Codec。
在 main()函数中除了解析传入参数并初始化 demuxer 与 muxer 的 parse_options( )函数以外,其他的功能都是在 av_encode( )函数里完成的。在 libavcodec\utils.c 中有如下二个函数 : AVCodec *avcodec_find_encoder(enum CodecID id)
和 AVCodec *avcodec_find_decoder(enum CodecID id)
他们的功能就是根据传入的 CodecID,找到匹配的 encoder 和 decoder。在 av_encode( )函数的开头,首先初始化各个 AVInputStream和 AVOutputStream,然后分别调用上述二个函数,并将匹配上的 encoder 与 decoder 分
别保存在:
1 | AVInputStream->AVStream *st->AVCodecContext *codec->struct AVCodec *codec |
AVCodecContext 结构
AVCodecContext 保存 AVCodec 指针和与 codec 相关数据,如 video 的 width、height,audio 的 sample rate 等。
AVCodecContext 中的 codec_type,codec_id 二个变量对于 encoder/decoder 的匹配来说,最为重要。
1 | enum CodecType codec_type; /* see CODEC_TYPE_xxx */ |
如上所示,codec_type 保存的是 CODEC_TYPE_VIDEO,CODEC_TYPE_AUDIO 等媒体类型,codec_id 保存的是 CODEC_ID_FLV1,CODEC_ID_VP6F 等编码方式。
以支持 flv 格式为例,在前述的 av_open_input_file(…… ) 函数中,匹配到正确的 AVInputFormat demuxer 后,通过 av_open_input_stream( )函数中调用 AVInputFormat的 read_header 接口来执行 flvdec.c 中的 flv_read_header( )函数。flv_read_header( )函数内,根据文件头中的数据,创建相应的视频或音频 AVStream,并设置 AVStream 中AVCodecContext 的正确的 codec_type 值。codec_id 值是在解码过程。flv_read_packet( )
函数执行时根据每一个 packet 头中的数据来设置的。
以 avidec 为例 有如下初始化,我们主要知道的就是 code_id 和 code_type 该字段关联具体的解码器,和解码类型(音视频或 subtitle)
1 | if (st->codec->stream_codec_tag == AV_RL32("Axan")) |
AVStream 结构保存与数据流相关的编解码器,数据段等信息。比较重要的有如下二个成员:
1 | AVCodecContext *codec; /**< codec context */ |
其中 codec 指针保存的就是上节所述的 encoder 或 decoder 结构。priv_data 指针保存的是和具体编解码流相关的数据,如下代码所示,在 ASF 的解码过程中,priv_data保存的就是 ASFStream 结构的数据。
1 | AVStream *st; |
根据输入和输出流的不同,前述的 AVStream 结构都是封装在 AVInputStream 和AVOutputStream 结构中,在 av_encode( )函数中使用。AVInputStream 中还保存的有与时间有关的信息。AVOutputStream 中还保存有与音视频同步等相关的信息。
AVPacket 结构定义如下,其是用于保存读取的 packet 数据。
1 | typedef struct AVPacket |
在 av_encode() 函数中,调用 AVInputFormat 的 (*read_packet)(struct AVFormatContext *, AVPacket *pkt)
接口,读取输入文件的一帧数据保存在当前输入 AVFormatContext 的 AVPacket 成员中。
本文对 ffmpeg 进行裁剪采用的是配置所需的接口,不需要的不配置,而不是采用修改源代码的方式。
在 linux 下进入终端,找到 ffmpeg 解压位置,输入如下命令:
1 | ./configure –help |
得到 configure 的基本选项参数,其并没有中文解释。
1 | --help 显示此帮助信息|print this message |
以下为配置 ffmpeg 的基本选项,其含义如下:
1 | --cache-file=FILE |
configure 会在你的系统上测试存在的特性(或者 bug!)。为了加速随后进行的配置,测试的结果会存储在一个 cache file 里。当 configure 到每个子树里都有 configure 脚本的复杂的源码树时,一个很好的 cache file 的存在会有很大帮助。
1 | --help |
输出帮助信息。即使是有经验的用户也偶尔需要使用使用 --help
选项,因为一个复杂的项目会包含附加的选项。例如,GCC 包里的 configure 脚本就包含了允许你控制是否生成和在 GCC 中使用 GNU 汇编器的选项。
1 | --no-create |
configure 中的一个主要函数会制作输出文件。此选项阻止 configure 生成这个文件。你可以认为这是一种演习(dry run),尽管缓存(cache)仍然被改写了。
1 | --quiet |
当 configure 进行他的测试时,会输出简要的信息来告诉用户正在作什么。这样做是因为 configure 可能会比较慢,没有这种输出的话用户将会被扔在一旁疑惑正在发生什么。使用这两个选项中的任何一个都会把你扔到一旁。(译注:这两句话比较有意思,原文是这样的:If there was no such output, the user would be left wondering what is happening. By using this option, you too can be left wondering!)
1 | --version |
打印用来产生 ‘configure’ 脚本的 Autoconf 的版本号。
1 | --prefix=PEWFIX |
--prefix
是最常用的选项。制作出的 Makefile 会查看随此选项传递的参数,当一个包在安装时可以彻底的重新安置他的结构独立部分。举一个例子,当安装一个包,例如说Emacs,下面的命令将会使 Emacs Lisp file 被安装到”/opt/gnu/share”:
1 | ./configure --prefix=/opt/gnu |
1 | --exec-prefix=EPREFIX |
与 --prefix
选项类似,但是他是用来设置结构倚赖的文件的安装位置。编译好的 emacs 二进制文件就是这样一个问件。如果没有设置这个选项的话,默认使用的选项值将被设为和 --prefix
选项值一样。
1 | --bindir=DIR |
指定二进制文件的安装位置。这里的二进制文件定义为可以被用户直接执行的程序。
1 | --sbindir=DIR |
指定超级二进制文件的安装位置。这是一些通常只能由超级用户执行的程序。
1 | --libexecdir=DIR |
指定可执行支持文件的安装位置。与二进制文件相反,这些文件从来不直接由用户执行,但是可以被上面提到的二进制文件所执行。
1 | --datadir=DIR |
指定通用数据文件的安装位置。
1 | --sysconfdir=DIR |
指定在单个机器上使用的只读数据的安装位置。
1 | --sharedstatedir=DIR |
指定可以在多个机器上共享的可写数据的安装位置。
1 | --localstatedir=DIR |
指定只能单机使用的可写数据的安装位置。
1 | --libdir=DIR |
指定库文件的安装位置。
1 | --includedir=DIR |
指定 C 头文件的安装位置。其他语言如 C++的头文件也可以使用此选项。
1 | --oldincludedir=DIR |
指定为除 GCC 外编译器安装的 C 头文件的安装位置。
1 | --infodir=DIR |
指定 Info 格式文档的安装位置。Info 是被 GNU 工程所使用的文档格式。
1 | --mandir=DIR |
指定手册页的安装位置。
1 | --srcdir=DIR |
这个选项对安装没有作用。他会告诉 configure 源码的位置。一般来说不用指定此选项,因为 configure 脚本一般和源码文件在同一个目录下。
1 | --program-prefix=PREFIX |
指定将被加到所安装程序的名字上的前缀。例如,使用 --program-prefix=g
来 configure一个名为 tar 的程序将会使安装的程序被命名为 gtar。当和其他的安装选项一起使用时,这个选项只有当他被 Makefile.in 文件使用时才会工作。
1 | --program-suffix=SUFFIX |
指定将被加到所安装程序的名字上的后缀。
1 | --program-transform-name=PROGRAM |
这里的 PROGRAM 是一个 sed 脚本。当一个程序被安装时,他的名字将经过 sed -e PROGRAM
来产生安装的名字。
1 | --build=BUILD |
指定软件包安装的系统平台。如果没有指定,默认值将是 --host
选项的值。
1 | --host=HOST |
指定软件运行的系统平台。如果没有指定,将会运行 config.guess 来检测。
1 | --target=GARGET |
指定软件面向(target to)的系统平台。这主要在程序语言工具如编译器和汇编器上下文中起作用。如果没有指定,默认将使用 --host
选项的值。
1 | --disable-FEATURE |
一些软件包可以选择这个选项来提供为大型选项的编译时配置,例如使用 Kerberos认证系统或者一个实验性的编译器最优配置。如果默认是提供这些特性,可以使用 --disable-FEATURE
来禁用它,这里 FEATURE 是特性的名字。例如:
1 | ./configure --disable-gui |
1 | --enable-FEATURE[=ARG] |
相反的,一些软件包可能提供了一些默认被禁止的特性,可以使用 --enable-FEATURE
来起用它。这里 FEATURE 是特性的名字。一个特性可能会接受一个可选的参数。例如:
1 | ./configure --enable-buffers=128 |
--enable-FEATURE=no
与上面提到的 --disable-FEATURE
是同义的。
1 | --with-PACKAGE[=ARG] |
在自由软件社区里,有使用已有软件包和库的优秀传统。当用 configure 来配置一个源码树时,可以提供其他已经安装的软件包的信息。例如,倚赖于 Tcl 和 Tk 的 BLT 器件工具包。要配置 BLT,可能需要给 configure 提供一些关于我们把 Tcl 和 Tk 装的何处的信息:
1 | ./configure --with-tcl=/usr/local --with-tk=/usr/local |
--with-PACKAGE=no
与下面将提到的 --without-PACKAGE
是同义的。
1 | --without-PACKAGE |
有时候你可能不想让你的软件包与系统已有的软件包交互。例如,你可能不想让你的新编译器使用 GNU ld。通过使用这个选项可以做到这一点:
1 | ./configure --without-gnu-ld |
1 | --x-includes=DIR |
这个选项是 --with-PACKAGE
选项的一个特例。在 Autoconf 最初被开发出来时,流行使用 configure 来作为 Imake 的一个变通方法来制作运行于 X 的软件。–x-includes 选项提供了向 configure 脚本指明包含 X11 头文件的目录的方法。
1 | --x-libraries=DIR |
类似的,--x-libraries
选项提供了向 configure 脚本指明包含 X11 库的目录的方法。
对 ffmpeg 的裁剪优化主要是对 ffplay 的裁剪优化,我们制定的需求是能播放测试文件(视频为 mpeg4 编码、音频为 mp2 编码,且为 AVI 复用),根据需求,找到相应的选项,或禁用或启用,最后的命令如下:
1 | ./configure --disable-yasm --disable-parsers --disable-decoders |
其中针对需求,
--disable-parsers
为禁用所有解析器,
--disable-decoders
为禁用所有解码器,
--disable-encoders
为禁用所有编码器,
--enable-decoder=mpeg4
为启用 mpeg4 的编码器 ,
--disable-muxers
为禁用所有复用,
--disable-demuxers
为禁用所有解复用,
--enable-demuxer=avi
为启用 AVI 复用,
--enable-decoder=mp2
为启用 mp2 编码,
--disable-protocols
为禁用所有协议,
--enable-protocol=file
为启用文件协议,
--disable-filters
为禁用所有过滤器,
--disable-bsfs
为禁用所有码流过滤器。
通过以上配置之后,编译,安装,就生成了我们要求的 ffplay,其大小为 1.8M(1864012 字节)。此次是在 linux 环境下进行的,在以后的配置中,如果需要其他的什么编码器或什么的,按照选项要求进行配置即可。
前面已经提到本次裁剪优化的内容。经过裁剪优化之后,对其文件夹进行比较,主要有 3 个地方不同,分别是 config.fate、config.h 和 config.mak。在 config.fate 中,其记录的是配置命令,由于前后两次配置命令不同,故相应内容也不同。在config.h 中,其主要是根据配置命令来改变相应预定义的值,达到裁剪优化之效果。在 config.mak 中,改变的也是配置命令中需要改变的选项。
FFMpeg 中比较重要的函数以及数据结构如下:
1、数据结构:
(1) AVFormatContext
(2) AVOutputFormat
(3) AVInputFormat
(4) AVCodecContext
(5) AVCodec
(6) AVFrame
(7) AVPacket
(8) AVPicture
(9) AVStream
2、初始化函数:
(1) av_register_all()
(2) avcodec_open()
(3) avcodec_close()
(4) av_open_input_file()
(5) av_find_input_format()
(6) av_find_stream_info()
(7) av_close_input_file()
3、音视频编解码函数:
(1) avcodec_find_decoder()
(2) avcodec_alloc_frame()
(3) avpicture_get_size()
(4) avpicture_fill()
(5) img_convert()
(6) avcodec_alloc_context()
(7) avcodec_decode_video()
(8) av_free_packet()
(9) av_free()
4、文件操作:
(1) avnew_steam()
(2) av_read_frame()
(3) av_write_frame()
(4) dump_format()
5、其他函数:
(1) avpicture_deinterlace()
(2) ImgReSampleContext()
1 | git clone http://source.ffmpeg.org/git/ffmpeg.git ffmpeg |
注意:在执行各自的 configure 创建编译配置文件时,最好都强制带上 –enable-static 和 –enable-shared 参数以确保生成静态库和动态库。另外因为是在 Mac OS X 环境下编译,因此在各自编译完后,都要执行 sudo make install,安装到默认的 /usr/local 目录下相应位置(Mac OS X 下不推荐 /usr),因此不要在 configure 时指定 –prefix,就用默认的 /usr/local 目录前缀即可。完成编译安装后,FFmpeg 的头文件将会复制到 /usr/local/include 下面相应位置,静态库及动态库会被复制到 /usr/local/lib 目录下,FFmpeg 的可执行程序(ffmpeg、ffprobe、ffserver)会被复制到 /usr/local/bin 目录下,这样 FFmpeg 的开发环境就构建好了。
]]>RPC详解
远程过程调用(RPC)详解
RPC原理详解
RPC入门总结(一)RPC定义和原理
RPC框架设计和调用详解
TODO
]]>Libev
libev是一个开源的事件驱动库,基于epoll,kqueue等OS提供的基础设施。其以高效出名,它可以将IO事件,定时器,和信号统一起来,统一放在事件处理这一套框架下处理。基于Reactor模式,效率较高,并且代码精简(4.15版本8000多行),是学习事件驱动编程的很好的资源。
下载链接:http://software.schmorp.de/pkg/libev.html
Memcached
Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提供动态数据库驱动网站的速度。Memcached 基于一个存储键/值对的 hashmap。Memcached-1.4.7的代码量还是可以接受的,只有10K行左右。
下载地址:http://memcached.org/
Redis
Redis 是一个使用 C 语言写成的,开源的 key-value 数据库。Redis支持的操作和数据类型比Memcached要多,现在主要用于缓存,支持主从同步机制,Redis的学习可以参考<<Redis设计与实现>>一书。
下载地址:http://redis.io/
Webbench
Webbench是一个在linux下使用的非常简单的网站压测工具。它使用fork()模拟多个客户端同时访问我们设定的URL,测试网站在压力下工作的性能,最多可以模拟3万个并发连接去测试网站的负载能力。Webbench使用C语言编写, 代码实在太简洁,源码加起来不到600行。
下载链接:https://github.com/LippiOuYang/WebBenchl
APR(Apache Portable Runtime)
这是由 Apache 社区维护的 C 开源库,主要提供操作系统相关的功能(文件系统、进程、线程、用户、IPC)。此外还提供了一些网络相关的功能。
APR 原先是 Apache Web 服务器的一个组成部分,后来独立出来,成为一个单独的开源项目。
主页:https://apr.apache.org
Tinyhttpd
tinyhttpd是一个超轻量型Http Server,使用C语言开发,全部代码只有502行(包括注释),附带一个简单的Client,可以通过阅读这段代码理解一个 Http Server 的本质。
下载链接:https://github.com/LippiOuYang/Tinyhttpd
cJSON
cJSON是C语言中的一个JSON编解码器,非常轻量级,C文件只有500多行,速度也非常理想。
cJSON也存在几个弱点,虽然功能不是非常强大,但cJSON的小身板和速度是最值得赞赏的。其代码被非常好地维护着,结构也简单易懂,可以作为一个非常好的C语言项目进行学习。
项目主页:http://sourceforge.net/projects/cjson/
CMockery
cmockery是google发布的用于C单元测试的一个轻量级的框架。它很小巧,对其他开源包没有依赖,对被测试代码侵入性小。cmockery的源代码行数不到3K,你阅读一下will_return和mock的源代码就一目了然了。
主要特点:
下载链接:http://code.google.com/p/cmockery/downloads/list
Lua
Lua很棒,Lua是巴西人发明的,这些都令我不爽,但是还不至于脸红,最多眼红。
让我脸红的是Lua的源代码,百分之一百的ANSI C,一点都不掺杂。在任何支持ANSI C编译器的平台上都可以轻松编译通过。我试过,真是一点废话都没有。Lua的代码数量足够小,5.1.4仅仅1.5W行,去掉空白行和注释估计能到1W行。
下载地址:http://www.lua.org/
SQLite
SQLite是一个开源的嵌入式关系数据库,实现自包容、零配置、支持事务的SQL数据库引擎。 其特点是高度便携、使用方便、结构紧凑、高效、可靠。足够小,大致3万行C代码,250K。
下载地址:http://www.sqlite.org/ 。
UNIX v6
UNIX V6 的内核源代码包括设备驱动程序在内 约有1 万行,这个数量的源代码,初学者是能够充分理解的。有一种说法是一个人所能理解的代码量上限为1 万行,UNIX V6的内核源代码从数量上看正好在这个范围之内。看到这里,大家是不是也有“如果只有1万行的话没准儿我也能学会”的想法呢?
另一方面,最近的操作系统,例如Linux 最新版的内核源代码据说超过了1000 万行。就算不是初学者,想完全理解全部代码基本上也是不可能的。
下载地址:http://minnie.tuhs.org/cgi-bin/utree.pl?file=V6
NETBSD
NetBSD是一个免费的,具有高度移植性的 UNIX-like 操作系统,是现行可移植平台最多的操作系统,可以在许多平台上执行,从 64bit alpha 服务器到手持设备和嵌入式设备。NetBSD计划的口号是:”Of course it runs NetBSD”。它设计简洁,代码规范,拥有众多先进特性,使得它在业界和学术界广受好评。由于简洁的设计和先进的特征,使得它在生产和研究方面,都有卓越的表现,而且它也有受使用者支持的完整的源代码。许多程序都可以很容易地通过NetBSD Packages Collection获得。
下载地址:http://www.netbsd.org/
关于 C++ 框架、库和资源的一些汇总列表,内容包括:标准库、Web应用框架、人工智能、数据库、图片处理、机器学习、日志、代码分析等。
C++标准库,包括了STL容器,算法和函数等。
C++通用框架和库
音频,声音,音乐,数字化音乐库
生物信息,基因组学和生物技术
压缩和归档库
并发执行和多线程
数据库,SQL服务器,ODBC驱动程序和工具
调试库, 内存和资源泄露检测,单元测试
动力学仿真引擎
XML就是个垃圾,xml的解析很烦人,对于计算机它也是个灾难。这种糟糕的东西完全没有存在的理由了。-Linus Torvalds
一些有用的库或者工具,但是不适合上面的分类,或者还没有分类。
用于创建开发环境的软件
C/C++编译器列表
在线C/C++编译器列表
C/C++调试器列表
C/C++集成开发环境列表
提高质量,减少瑕疵的代码分析工具列表
感谢平凡之路和fffaraz 的整理,转载请注明出处。
]]>文章来源
这里收录比较实用的计算机相关技术书籍,可以在短期之内入门的简单实用教程、一些技术网站以及一些写的比较好的博文,欢迎Fork,你也可以通过Pull Request参与编辑。
csdn免积分下载器
其实csdn上资源还是挺多的,如何免积分下载是一个很大的问题
但是有了 http://www.itziy.com/ csdn免积分下载器 工具之后,一切问题迎刃而解
有了 http://www.itziy.com/ csdn免积分下载器 不再为csdn免积分下载而烦恼
http://www.itziy.com/ csdn免积分下载器 专为需要csdn免积分下载的朋友开设
Google JavaScript 代码风格指南
Google JSON 风格指南
Airbnb JavaScript 规范
JavaScript 标准参考教程(alpha)
Javascript编程指南 (源码)
javascript 的 12 个怪癖
JavaScript 秘密花园
JavaScript核心概念及实践 (PDF) (此书已由人民邮电出版社出版发行,但作者依然免费提供PDF版本,希望开发者们去购买,支持作者)
《JavaScript 模式》 “JavaScript patterns”中译本
命名函数表达式探秘 (注:原文由为之漫笔翻译,原始地址无法打开,所以此处地址为我博客上的备份)
学用 JavaScript 设计模式 (开源中国)
深入理解JavaScript系列
ECMAScript 6 入门 (作者:阮一峰)
JavaScript Promise迷你书
You-Dont-Know-JS (深入JavaScript语言核心机制的系列图书)
JavaScript 教程 廖雪峰
MDN JavaScript 中文文档
jQuery
Node.js
underscore.js
backbone.js
AngularJS
Zepto.js
Sea.js
React.js
impress.js
CoffeeScript
TypeScipt
ExtJS
Meteor
Chrome扩展及应用开发
视频地址
Github地址
快捷键 | 说明 |
---|---|
, | Leader Key |
函数跳转 | |
<leader>n | 打开/关闭代码资源管理器 |
<leader>t | 打开/关闭函数列表 |
<leader>a | .h .cpp 文件切换 |
<leader>u | 转到函数声明 |
<leader>U | 转到函数实现 |
g] | 声明/定义跳转 |
<leader>o | 打开include文件 |
<leader>y | 拷贝函数声明 |
<leader>p | 生成函数实现 |
<c-p> | 切换到上一个buffer |
<c-n> | 切换到下一个buffer |
:e <filename> | 新建buffer打开文件 |
Ctrl + ] | 找到光标所在位置的标签定义的地方 |
Ctrl + t | 回到跳转之前的标签处 |
Ctrl + o | 退回原来的地方 |
辅助操作 | |
gcc | 注释代码 |
gcap | 注释段落 |
za | 打开或关闭当前折叠 |
zM | 关闭所有折叠 |
zR | 打开所有折叠 |
<c-w>h | 跳到左边的窗口 |
<c-w>j | 跳到下边的窗口 |
<c-w>k | 跳到上边的窗口 |
<c-w>l | 跳到右边的窗口 |
<c-w>c | 关闭当前窗口 |
<c-w>o | 关闭其他窗口 |
:only | 关闭其他窗口 |
快捷键 | 说明 |
---|---|
<leader>f | 搜索~目录下的文件 |
<leader>F | 搜索当前目录下的文本 |
<leader>g | 显示git仓库提交记录 |
<leader>G | 显示当前文件提交记录 |
<leader>gg | 显示当前文件在某个commit下的完整内容 |
<leader>d | 删除当前buffer |
<leader>D | 删除当前buffer外的所有buffer |
Ya | 复制行文本到字母a |
Da | 剪切行文本到字母a |
Ca | 改写行文本到字母a |
rr | 替换文本 |
<leader>r | 全局替换,目前只支持单个文件 |
vif | 选中函数内容 |
dif | 删除函数内容 |
cif | 改写函数内容 |
vaf | 选中函数内容(包括函数名 花括号) |
daf | 删除函数内容(包括函数名 花括号) |
caf | 改写函数内容(包括函数名 花括号) |
:sp <filename> | 横向切分窗口并打开文件 |
:vsp <filename> | 竖向切分窗口并打开文件 |
gg=G | 缩进整个文件 |
=a{ | 缩进光标所在代码块 |
=i{ | 缩进光标所在代码块,不缩进”{“ |
<< | 减少缩进 |
>> | 增加缩进 |
== | 自动缩进 |
ctrl+f | 下翻一屏 |
ctrl+b | 上翻一屏 |
ctrl+d | 下翻半屏 |
ctrl+u | 上翻半屏 |
s | 替换字符(删除光标处字符,并进入插入模式,前可接数量) |
S | 替换行(删除当前行,并进入插入模式,前可接数量) |
cc | 改写当前行(删除当前行并进入插入模式),同 S |
cw | 改写光标开始处的当前单词 |
ciw | 改写光标所处的单词 |
caw | 改写光标所处的单词,并且包括前后空格(如果有的话) |
ct, | 改写到逗号 |
c0 | 改写到行首 |
c^ | 改写到行首(第一个非零字符) |
c$ | 改写到行末 |
C | 改写到行末(同 c$) |
ci" | 改写双引号中的内容 |
ci' | 改写单引号中的内容 |
ci) | 改写小括号中的内容 |
ci] | 改写中括号中内容 |
ci} | 改写大括号中内容 |
cit | 改写 xml tag 中的内容 |
cis | 改写当前句子 |
ciB | 改写’{}’中的内容 |
c2w | 改写下两个单词 |
ct( | 改写到小括号前 |
x | 删除当前字符,前面可以接数字,3x代表删除三个字符 |
X | 向前删除字符 |
dd | 删除当前行 |
d0 | 删除到行首 |
d^ | 删除到行首(第一个非零字符) |
d$ | 删除到行末 |
D | 删除到行末(同 d$) |
dw | 删除当前单词 |
dt, | 删除到逗号 |
diw | 删除光标所处的单词 |
daw | 删除光标所处的单词,并包含前后空格(如果有的话) |
di" | 删除双引号中的内容 |
di' | 删除单引号中的内容 |
di) | 删除小括号中的内容 |
di] | 删除中括号中内容 |
di} | 删除大括号中内容 |
diB | 删除’{}’中的内容 |
dit | 删除 xml tag 中的内容 |
dis | 删除当前句子 |
d2w | 删除下两个单词 |
dt( | 删除到小括号前 |
dgg | 删除到文件头部 |
dG | 删除到文件尾部 |
d} | 删除下一段 |
d{ | 删除上一段 |
u | 撤销 |
U | 撤销整行操作 |
CTRL-R | 撤销上一次 u 命令 |
J | 连接若干行 |
gJ | 连接若干行,删除空白字符 |
. | 重复上一次操作 |
~ | 交换大小写 |
g~iw | 替换当前单词的大小写 |
gUiw | 将单词转成大写 |
guiw | 将当前单词转成小写 |
guu | 全行转为小写 |
gUU | 全行转为大写 |
gg=G | 缩进整个文件 |
=a{ | 缩进光标所在代码块 |
=i{ | 缩进光标所在代码块,不缩进”{“ |
<< | 减少缩进 |
>> | 增加缩进 |
== | 自动缩进 |
CTRL-A | 增加数字 |
CTRL-X | 减少数字 |
p | 粘贴到光标后 |
P | 粘贴到光标前 |
v | 开始标记 |
y | 复制标记内容 |
V | 开始按行标记 |
CTRL-V | 开始列标记 |
y$ | 复制当前位置到本行结束的内容 |
yy | 复制当前行 |
Y | 复制当前行,同 yy |
yt, | 复制到逗号 |
yiw | 复制当前单词 |
3yy | 复制光标下三行内容 |
v0 | 选中当前位置到行首 |
v$ | 选中当前位置到行末 |
vt, | 选中到逗号 |
viw | 选中当前单词 |
vi) | 选中小括号内的东西 |
vi] | 选中中括号内的东西 |
viB | 选中’{}’中的内容 |
vis | 选中句子中的东西 |
gv | 重新选择上一次选中的文字 |
:set paste | 允许粘贴模式(避免粘贴时自动缩进影响格式) |
:set nopaste | 禁止粘贴模式 |
"?yy | 复制当前行到寄存器 ? ,问号代表 0-9 的寄存器名称 |
"?p | 将寄存器 ? 的内容粘贴到光标后 |
"?P | 将寄存器 ? 的内容粘贴到光标前 |
:registers | 显示所有寄存器内容 |
:[range]y | 复制范围,比如 :20,30y 是复制20到30行,:10y 是复制第十行 |
:[range]d | 删除范围,比如 :20,30d 是删除20到30行,:10d 是删除第十行 |
ddp | 交换两行内容:先删除当前行复制到寄存器,并粘贴 |
/pattern | 从光标处向文件尾搜索 pattern |
?pattern | 从光标处向文件头搜索 pattern |
n | 向同一方向执行上一次搜索 |
N | 向相反方向执行上一次搜索 |
* | 向前搜索光标下的单词 |
# | 向后搜索光标下的单词 |
:s/p1/p2/g | 替换当前行的p1为p2 |
:%s/p1/p2/g | 替换当前文件中的p1为p2 |
:%s/<p1>/p2/g | 替换当前文件中的p1单词为p2 |
:%s/p1/p2/gc | 替换当前文件中的p1为p2,并且每处询问你是否替换 |
:10,20s/p1/p2/g | 将第10到20行中所有p1替换为p2 |
:%s/1\\2\/3/123/g | 将“1\2/3” 替换为 “123”(特殊字符使用反斜杠标注) |
:%s/\r//g | 删除 DOS 换行符 ^M |
:g/^\s*$/d | 删除空行 |
:g/test/d | 删除所有包含 test 的行 |
:v/test/d | 删除所有不包含 test 的行 |
:%s/^/test/ | 在行首加入特定字符(也可以用宏录制来添加) |
:%s/$/test/ | 在行尾加入特定字符(也可以用宏录制来添加) |
:sort | 排序 |
:g/^\(.\+\)$\n\1/d | 去除重复行(先排序) |
:%s/^.\{10\}// | 删除每行前10个字符 |
:%s/.\{10\}$// | 删除每行尾10个字符 |
vimplus github
快捷键 | 说明 |
---|---|
, | Leader Key |
<leader>n | 打开/关闭代码资源管理器 |
<leader>t | 打开/关闭函数列表 |
<leader>a | .h .cpp 文件切换 |
<leader>u | 转到函数声明 |
<leader>U | 转到函数实现 |
<leader>o | 打开include文件 |
<leader>y | 拷贝函数声明 |
<leader>p | 生成函数实现 |
<leader>w | 单词跳转 |
<leader>f | 搜索~目录下的文件 |
<leader>F | 搜索当前目录下的文本 |
<leader>g | 显示git仓库提交记录 |
<leader>G | 显示当前文件提交记录 |
<leader>gg | 显示当前文件在某个commit下的完整内容 |
<leader>ff | 语法错误自动修复(FixIt) |
<c-p> | 切换到上一个buffer |
<c-n> | 切换到下一个buffer |
<leader>d | 删除当前buffer |
<leader>D | 删除当前buffer外的所有buffer |
Ya | 复制行文本到字母a |
Da | 剪切行文本到字母a |
Ca | 改写行文本到字母a |
rr | 替换文本 |
<leader>r | 全局替换,目前只支持单个文件 |
gcc | 注释代码 |
gcap | 注释段落 |
vif | 选中函数内容 |
dif | 删除函数内容 |
cif | 改写函数内容 |
vaf | 选中函数内容(包括函数名 花括号) |
daf | 删除函数内容(包括函数名 花括号) |
caf | 改写函数内容(包括函数名 花括号) |
fa | 查找字母a,然后再按f键查找下一个 |
快捷键 | 说明 |
---|---|
za | 打开或关闭当前折叠 |
zM | 关闭所有折叠 |
zR | 打开所有折叠 |
快捷键 | 说明 |
---|---|
g] | 声明/定义跳转 |
快捷键 | 说明 |
---|---|
:e <filename> | 新建buffer打开文件 |
:bp | 切换到上一个buffer |
:bn | 切换到下一个buffer |
:bd | 删除当前buffer |
快捷键 | 说明 |
---|---|
:sp <filename> | 横向切分窗口并打开文件 |
:vsp <filename> | 竖向切分窗口并打开文件 |
<c-w>h | 跳到左边的窗口 |
<c-w>j | 跳到下边的窗口 |
<c-w>k | 跳到上边的窗口 |
<c-w>l | 跳到右边的窗口 |
<c-w>c | 关闭当前窗口 |
<c-w>o | 关闭其他窗口 |
:only | 关闭其他窗口 |
快捷键 | 说明 |
---|---|
h | 上下左右移动 |
j | 上下左右移动 |
k | 上下左右移动 |
l | 上下左右移动 |
0 | 光标移动到行首 |
^ | 跳到从行首开始第一个非空白字符 |
$ | 光标移动到行尾 |
<c-o> | 跳到上一个位置 |
<c-i> | 跳到下一个位置 |
<c-b> | 上一页 |
<c-f> | 下一页 |
<c-u> | 上移半屏 |
<c-d> | 下移半屏 |
H | 调到屏幕顶上 |
M | 调到屏幕中间 |
L | 调到屏幕下方 |
:n | 跳到第n行 |
w | 跳到下一个单词开头(标点或空格分隔的单词) |
W | 跳到下一个单词开头(空格分隔的单词) |
e | 跳到下一个单词尾部(标点或空格分隔的单词) |
E | 跳到下一个单词尾部(空格分隔的单词) |
b | 上一个单词头(标点或空格分隔的单词) |
B | 上一个单词头(空格分隔的单词) |
ge | 上一个单词尾 |
% | 在配对符间移动, 可用于()、{}、[] |
gg | 到文件首 |
G | 到文件尾 |
fx | 跳转到下一个为x的字符 |
Fx | 跳转到上一个为x的字符 |
tx | 跳转到下一个为x的字符前 |
Tx | 跳转到上一个为x的字符前 |
; | 跳到下一个搜索的结果 |
[[ | 跳转到函数开头 |
]] | 跳转到函数结尾 |
快捷键 | 说明 |
---|---|
r | 替换当前字符 |
R | 进入替换模式,直至 ESC 离开 |
s | 替换字符(删除光标处字符,并进入插入模式,前可接数量) |
S | 替换行(删除当前行,并进入插入模式,前可接数量) |
cc | 改写当前行(删除当前行并进入插入模式),同 S |
cw | 改写光标开始处的当前单词 |
ciw | 改写光标所处的单词 |
caw | 改写光标所处的单词,并且包括前后空格(如果有的话) |
ct, | 改写到逗号 |
c0 | 改写到行首 |
c^ | 改写到行首(第一个非零字符) |
c$ | 改写到行末 |
C | 改写到行末(同 c$) |
ci" | 改写双引号中的内容 |
ci' | 改写单引号中的内容 |
ci) | 改写小括号中的内容 |
ci] | 改写中括号中内容 |
ci} | 改写大括号中内容 |
cit | 改写 xml tag 中的内容 |
cis | 改写当前句子 |
ciB | 改写’{}’中的内容 |
c2w | 改写下两个单词 |
ct( | 改写到小括号前 |
x | 删除当前字符,前面可以接数字,3x代表删除三个字符 |
X | 向前删除字符 |
dd | 删除当前行 |
d0 | 删除到行首 |
d^ | 删除到行首(第一个非零字符) |
d$ | 删除到行末 |
D | 删除到行末(同 d$) |
dw | 删除当前单词 |
dt, | 删除到逗号 |
diw | 删除光标所处的单词 |
daw | 删除光标所处的单词,并包含前后空格(如果有的话) |
di" | 删除双引号中的内容 |
di' | 删除单引号中的内容 |
di) | 删除小括号中的内容 |
di] | 删除中括号中内容 |
di} | 删除大括号中内容 |
diB | 删除’{}’中的内容 |
dit | 删除 xml tag 中的内容 |
dis | 删除当前句子 |
d2w | 删除下两个单词 |
dt( | 删除到小括号前 |
dgg | 删除到文件头部 |
dG | 删除到文件尾部 |
d} | 删除下一段 |
d{ | 删除上一段 |
u | 撤销 |
U | 撤销整行操作 |
CTRL-R | 撤销上一次 u 命令 |
J | 连接若干行 |
gJ | 连接若干行,删除空白字符 |
. | 重复上一次操作 |
~ | 交换大小写 |
g~iw | 替换当前单词的大小写 |
gUiw | 将单词转成大写 |
guiw | 将当前单词转成小写 |
guu | 全行转为小写 |
gUU | 全行转为大写 |
gg=G | 缩进整个文件 |
=a{ | 缩进光标所在代码块 |
=i{ | 缩进光标所在代码块,不缩进”{“ |
<< | 减少缩进 |
>> | 增加缩进 |
== | 自动缩进 |
CTRL-A | 增加数字 |
CTRL-X | 减少数字 |
p | 粘贴到光标后 |
P | 粘贴到光标前 |
v | 开始标记 |
y | 复制标记内容 |
V | 开始按行标记 |
CTRL-V | 开始列标记 |
y$ | 复制当前位置到本行结束的内容 |
yy | 复制当前行 |
Y | 复制当前行,同 yy |
yt, | 复制到逗号 |
yiw | 复制当前单词 |
3yy | 复制光标下三行内容 |
v0 | 选中当前位置到行首 |
v$ | 选中当前位置到行末 |
vt, | 选中到逗号 |
viw | 选中当前单词 |
vi) | 选中小括号内的东西 |
vi] | 选中中括号内的东西 |
viB | 选中’{}’中的内容 |
vis | 选中句子中的东西 |
gv | 重新选择上一次选中的文字 |
:set paste | 允许粘贴模式(避免粘贴时自动缩进影响格式) |
:set nopaste | 禁止粘贴模式 |
"?yy | 复制当前行到寄存器 ? ,问号代表 0-9 的寄存器名称 |
"?p | 将寄存器 ? 的内容粘贴到光标后 |
"?P | 将寄存器 ? 的内容粘贴到光标前 |
:registers | 显示所有寄存器内容 |
:[range]y | 复制范围,比如 :20,30y 是复制20到30行,:10y 是复制第十行 |
:[range]d | 删除范围,比如 :20,30d 是删除20到30行,:10d 是删除第十行 |
ddp | 交换两行内容:先删除当前行复制到寄存器,并粘贴 |
快捷键 | 说明 |
---|---|
:e <filename> | 打开文件并编辑 |
:saveas <filename> | 另存为文件 |
:close | 关闭文件 |
:wa | 保存所有文件 |
:new | 打开一个新的窗口编辑新文件 |
:enew | 在当前窗口创建新文件 |
:vnew | 在左右切分的新窗口中编辑新文件 |
:tabnew | 在新的标签页中编辑新文件 |
快捷键 | 说明 |
---|---|
! | 告诉vim正在执行一个过滤操作 |
!5Gsort<Enter> | 使用外部sort命令对1-5行文本排序 |
!! | 对当前行执行过滤命令 |
!!date<Enter> | 用”date”的输出代替当前行 |
快捷键 | 说明 |
---|---|
qa | 开始录制名字为a的宏 |
q | 结束录制宏 |
@a | 播放名字为a的宏 |
100@a | 播放名字为a的宏100次 |
:normal@a | 播放名字为a的宏直到自动结束 |
快捷键 | 说明 |
---|---|
/pattern | 从光标处向文件尾搜索 pattern |
?pattern | 从光标处向文件头搜索 pattern |
n | 向同一方向执行上一次搜索 |
N | 向相反方向执行上一次搜索 |
* | 向前搜索光标下的单词 |
# | 向后搜索光标下的单词 |
:s/p1/p2/g | 替换当前行的p1为p2 |
:%s/p1/p2/g | 替换当前文件中的p1为p2 |
:%s/<p1>/p2/g | 替换当前文件中的p1单词为p2 |
:%s/p1/p2/gc | 替换当前文件中的p1为p2,并且每处询问你是否替换 |
:10,20s/p1/p2/g | 将第10到20行中所有p1替换为p2 |
:%s/1\\2\/3/123/g | 将“1\2/3” 替换为 “123”(特殊字符使用反斜杠标注) |
:%s/\r//g | 删除 DOS 换行符 ^M |
:g/^\s*$/d | 删除空行 |
:g/test/d | 删除所有包含 test 的行 |
:v/test/d | 删除所有不包含 test 的行 |
:%s/^/test/ | 在行首加入特定字符(也可以用宏录制来添加) |
:%s/$/test/ | 在行尾加入特定字符(也可以用宏录制来添加) |
:sort | 排序 |
:g/^\(.\+\)$\n\1/d | 去除重复行(先排序) |
:%s/^.\{10\}// | 删除每行前10个字符 |
:%s/.\{10\}$// | 删除每行尾10个字符 |
快捷键 | 说明 |
---|---|
h tutor | 入门文档 |
h quickref | 快速帮助 |
h index | 查询Vim所有键盘命令定义 |
h summary | 帮助你更好的使用内置帮助系统 |
h pattern.txt | 正则表达式帮助 |
h eval | 脚本编写帮助 |
h function-list | 查看VimScript的函数列表 |
h windows.txt | 窗口使用帮助 |
h tabpage.txt | 标签页使用帮助 |
h tips | 查看Vim内置的常用技巧文档 |
h quote | 寄存器 |
h autocommand-events | 所有可能事件 |
h write-plugin | 编写插件 |
快捷键 | 说明 |
---|---|
vim -u NONE -N | 开启vim时不加载vimrc文件 |
vimdiff file1 file2 | 显示文件差异 |
<leader>e | 快速编辑vimrc文件 |
<leader>s | 重新加载vimrc文件 |
<leader>h | 打开vimplus帮助文档 |
<leader>H | 打开当前光标所在单词的vim帮助文档 |
<leader><leader>i | 安装插件 |
<leader><leader>u | 更新插件 |
<leader><leader>c | 删除插件 |
vimplus github
Vim使用笔记
利用ctags+cscope+taglist+nerdree+srcexpl+trinity 将 VIM 变成 source insight
:e
– 重新加载当前文档。:e!
– 重新加载当前文档,并丢弃已做的改动。:e file
– 关闭当前编辑的文件,并开启新的文件。 如果对当前文件的修改未保存,vi 会警告。:e! file
– 放弃对当前文件的修改,编辑新的文件。:e# 或 ctrl+^
– 回到刚才编辑的文件,很实用。gf
– 打开以光标所在字符串为文件名的文件。:saveas newfilename
– 另存为gj
: 移动到一段内的下一行;gk
: 移动到一段内的上一行;w
: 前移一个单词,光标停在下一个单词开头;b
: 后移一个单词,光标停在上一个单词开头;(
: 前移1句。)
: 后移1句。{
: 前移1段。}
: 后移1段。fc
: 把光标移到同一行的下一个 c 字符处Fc
: 把光标移到同一行的上一个 c 字符处tc
: 把光标移到同一行的下一个 c 字符前Tc
: 把光标移到同一行的上一个 c 字符后;
: 配合 f & t
使用,重复一次,
: 配合 f & t
使用,反向重复一次上面的操作都可以配合 n 使用,比如在正常模式(下面会讲到)下输入3h, 则光标向左移动 3 个字符。
0
: 移动到行首。g0
: 移到光标所在屏幕行行首。^
: 移动到本行第一个非空白字符。g^
: 同 ^
,但是移动到当前屏幕行第一个非空字符处。$
: 移动到行尾。g$
: 移动光标所在屏幕行行尾。n|
: 把光标移到递 n 列上。nG
: 到文件第 n 行。:n<cr>
: 移动到第 n 行。:$<cr>
: 移动到最后一行。H
: 把光标移到屏幕最顶端一行。M
: 把光标移到屏幕中间一行。L
: 把光标移到屏幕最底端一行。gg
: 到文件头部。G
: 到文件尾部。ctrl+f
: 下翻一屏。ctrl+b
: 上翻一屏。ctrl+d
: 下翻半屏。ctrl+u
: 上翻半屏。ctrl+e
: 向下滚动一行。ctrl+y
: 向上滚动一行。n%
: 到文件 n%
的位置。zz
: 将当前行移动到屏幕中央。zt
: 将当前行移动到屏幕顶端。zb
: 将当前行移动到屏幕底端。使用标记可以快速移动。到达标记后,可以用 Ctrl+o
返回原来的位置。 Ctrl+o
和 Ctrl+i
很像浏览器上的 后退 和 前进 。
m{a-z}
: 标记光标所在位置,局部标记,只用于当前文件。m{A-Z}
: 标记光标所在位置,全局标记。标记之后,退出Vim, 重新启动,标记仍然有效。'{a-z}
: 移动到标记行的行首。:marks
– 显示所有标记。:delmarks a b
– 删除标记 a 和 b。:delmarks a-c
– 删除标记 a、b 和 c。:delmarks a c-f
– 删除标记 a、c、d、e、f。:delmarks!
– 删除当前缓冲区的所有标记。:help mark-motions
– 查看更多关于 mark 的知识。i
: 在光标前插入;一个小技巧:按 8,再按 i
,进入插入模式,输入 =
, 按 esc
进入命令模式,就会出现 8 个 =
。 这在插入分割线时非常有用,如30i+<esc>
就插入了 36 个 +
组成的分割线。:r filename
: 在当前位置插入另一个文件的内容。:r !date
: 在光标处插入当前日期与时间。同理,:r !command
可以将其它 shell 命令的输出插入当前文档。c[n]w
: 改写光标后 1(n) 个词。c[n]l
: 改写光标后 n 个字母。c[n]h
: 改写光标前 n 个字母。[n]cc
: 修改当前 [n] 行。[n]s
: 以输入的文本替代光标之后 1(n) 个字符,相当于 c[n]l
。[n]S
: 删除指定数目的行,并以所输入文本代替之。注意,类似 cnw,dnw,ynw
的形式同样可以写为 ncw,ndw,nyw
。
[n]x
: 剪切光标右边 n 个字符,相当于 d[n]l
。[n]X
: 剪切光标左边 n 个字符,相当于 d[n]h
。y
: 复制在可视模式下选中的文本。yy or Y
: 复制整行文本。y[n]w
: 复制一 (n) 个词。y[n]l
: 复制光标右边 1(n) 个字符。y[n]h
: 复制光标左边 1(n) 个字符。y$
: 从光标当前位置复制到行尾。y0
: 从光标当前位置复制到行首。:m,ny<cr>
: 复制 m 行到 n 行的内容。y1G 或 ygg
: 复制光标以上的所有行。yG
: 复制光标以下的所有行。yaw 和 yas
:复制一个词和复制一个句子,即使光标不在词首和句首也没关系。d
: 删除(剪切)在可视模式下选中的文本。d$ or D
: 删除(剪切)当前位置到行尾的内容。d[n]w
: 删除(剪切)1(n)个单词d[n]l
: 删除(剪切)光标右边 1(n) 个字符。d[n]h
: 删除(剪切)光标左边 1(n) 个字符。d0
: 删除(剪切)当前位置到行首的内容[n] dd
: 删除(剪切)1(n) 行。:m,nd<cr>
: 剪切 m 行到 n 行的内容。d1G 或 dgg
: 剪切光标以上的所有行。dG
: 剪切光标以下的所有行。daw 和 das
:剪切一个词和剪切一个句子,即使光标不在词首和句首也没关系。d/f<cr>
:这是一个比较高级的组合命令,它将删除当前位置 到下一个 f 之间的内容。p
: 在光标之后粘贴。P
: 在光标之前粘贴。aw
:一个词as
:一句。ap
:一段。ab
:一块(包含在圆括号中的)。y, d, c, v
都可以跟文本对象。
a-z
:都可以用作寄存器名。"ayy
把当前行的内容放入 a 寄存器。A-Z
:用大写字母索引寄存器,可以在寄存器中追加内容。 如 "Ayy
把当前行的内容追加到 a 寄存器中。:reg
: 显示所有寄存器的内容。""
:不加寄存器索引时,默认使用的寄存器。"*
:当前选择缓冲区,"*yy
把当前行的内容放入当前选择缓冲区。"+
:系统剪贴板。"+yy
把当前行的内容放入系统剪贴板。/something
: 在后面的文本中查找 something。?something
: 在前面的文本中查找 something。/pattern/+number
: 将光标停在包含 pattern 的行后面第 number 行上。/pattern/-number
: 将光标停在包含 pattern 的行前面第 number 行上。n
: 向后查找下一个。N
: 向前查找下一个。可以用 grep 或 vimgrep 查找一个模式都在哪些地方出现过,其中 :grep
是调用外部的 grep 程序,而 :vimgrep
是 vim 自己的查找算法。
用法为: :vim[grep]/pattern/[g] [j] files
g
的含义是如果一个模式在一行中多次出现,则这一行也在结果中多次出现。
j
的含义是 grep 结束后,结果停在第 j 项,默认是停在第一项。
vimgrep 前面可以加数字限定搜索结果的上限,如 :1vim/pattern/ %
只查找那个模式在本文件中的第一个出现。
其实 vimgrep 在读纯文本电子书时特别有用,可以生成导航的目录。
比如电子书中每一节的标题形式为:n. xxxx
。你就可以这样::vim/^d{1,}./ %
然后用 :cw
或 :copen
查看结果,可以用 C-w H
把 quickfix 窗口移到左侧,就更像个目录了。
:s/old/new
– 用 new 替换当前行第一个 old。:s/old/new/g
– 用 new 替换当前行所有的 old。:n1,n2s/old/new/g
– 用 new 替换文件 n1 行到 n2 行所有的 old。:%s/old/new/g
– 用 new 替换文件中所有的 old。:%s/^/xxx/g
– 在每一行的行首插入 xxx,^
表示行首。:%s/$/xxx/g
– 在每一行的行尾插入 xxx,$
表示行尾。%s/old/new/gc
,加上i则忽略大小写(ignore)。还有一种比替换更灵活的方式,它是匹配到某个模式后执行某种命令,
语法为 :[range]g/pattern/command
例如 : %g/^ xyz/normal dd
。
表示对于以一个空格和 xyz 开头的行执行 normal 模式下的 dd 命令。
关于 range 的规定为:
m,n
: 从 m 行到 n 行。0
: 最开始一行(可能是这样)。$
: 最后一行.
: 当前行%
: 所有行高级的查找替换就要用到正则表达式。
\d
: 表示十进制数(我猜的)\s
: 表示空格\S
: 非空字符\a
: 英文字母\|
: 表示 或\.
: 表示.{m,n}
: 表示 m 到 n 个字符。这要和 \s
与 \a
等连用,如 \a\{m,n}
表示 m 到 n 个英文字母。{m,}
: 表示 m 到无限多个字符。**
: 当前目录下的所有子目录。:help pattern
得到更多帮助。
我们可以一次打开多个文件,如
1 | vi a.txt b.txt c.txt |
:next(:n)
编辑下一个文件。:2n
编辑下 2 个文件。:previous或:N
编辑上一个文件。:wnext
,保存当前文件,并编辑下一个文件。:wprevious
,保存当前文件,并编辑上一个文件。:args
显示文件列表。:n filenames 或 :args filenames
指定新的文件列表。vi -o filenames
在水平分割的多个窗口中编辑多个文件。vi -O filenames
在垂直分割的多个窗口中编辑多个文件。vim -p files
: 打开多个文件,每个文件占用一个标签页。:tabe, tabnew
– 如果加文件名,就在新的标签中打开这个文件, 否则打开一个空缓冲区。^w gf
– 在新的标签页里打开光标下路径指定的文件。:tabn
– 切换到下一个标签。Control + PageDown
,也可以。:tabp
– 切换到上一个标签。Control + PageUp
,也可以。[n] gt
– 切换到下一个标签。如果前面加了 n , 就切换到第 n 个标签。第一个标签的序号就是 1。:tab split
– 将当前缓冲区的内容在新页签中打开。:tabc[lose]
– 关闭当前的标签页。:tabo[nly]
– 关闭其它的标签页。:tabs
– 列出所有的标签页和它们包含的窗口。:tabm[ove] [N]
– 移动标签页,移动到第N个标签页之后。 如 tabm 0 当前标签页,就会变成第一个标签页。:buffers 或 :ls 或 :files
显示缓冲区列表。ctrl+^
:在最近两个缓冲区间切换。:bn
– 下一个缓冲区。:bp
– 上一个缓冲区。:bl
– 最后一个缓冲区。:b[n] 或 :[n]b
– 切换到第 n 个缓冲区。:nbw(ipeout)
– 彻底删除第 n 个缓冲区。:nbd(elete)
– 删除第 n 个缓冲区,并未真正删除,还在 unlisted 列表中。:ba[ll]
– 把所有的缓冲区在当前页中打开,每个缓冲区占一个窗口。vim -o file1 file2
: 水平分割窗口,同时打开 file1 和 file2vim -O file1 file2
: 垂直分割窗口,同时打开 file1 和 file2:split(:sp)
– 把当前窗水平分割成两个窗口。(CTRL-W s
或 CTRL-W CTRL-S
) 注意如果在终端下,CTRL-S
可能会冻结终端,请按 CTRL-Q
继续。:split filename
– 水平分割窗口,并在新窗口中显示另一个文件。:nsplit(:nsp)
– 水平分割出一个 n 行高的窗口。:[N]new
– 水平分割出一个N行高的窗口,并编辑一个新文件。 ( CTRL-W n
或 CTRL-W CTRL-N
)ctrl+w f
–水平分割出一个窗口,并在新窗口打开名称为光标所在词的文件 。C-w C-^
– 水平分割一个窗口,打开刚才编辑的文件。:vsplit(:vsp)
– 把当前窗口分割成水平分布的两个窗口。 (CTRL-W v
或 CTRL CTRL-V
):[N]vne[w]
– 垂直分割出一个新窗口。:vertical 水平分割的命令
: 相应的垂直分割。:qall
– 关闭所有窗口,退出 vim。:wall
– 保存所有修改过的窗口。:only
– 只保留当前窗口,关闭其它窗口。(CTRL-W o
):close
– 关闭当前窗口,CTRL-W c
能实现同样的功能。 (象 :q :x
同样工作 )ctrl+w +
–当前窗口增高一行。也可以用 n 增高 n 行。ctrl+w -
–当前窗口减小一行。也可以用 n 减小 n 行。ctrl+w _
–当前窗口扩展到尽可能的大。也可以用 n 设定行数。:resize n
– 当前窗口 n 行高。ctrl+w =
– 所有窗口同样高度。n ctrl+w _
– 当前窗口的高度设定为 n 行。ctrl+w <
–当前窗口减少一列。也可以用 n 减少 n 列。ctrl+w >
–当前窗口增宽一列。也可以用 n 增宽 n 列。ctrl+w |
–当前窗口尽可能的宽。也可以用 n 设定列数。如果支持鼠标,切换和调整子窗口的大小就简单了。
ctrl+w ctrl+w
: 切换到下一个窗口。或者是 ctrl+w w
。ctrl+w p
: 切换到前一个窗口。ctrl+w h(l,j,k)
:切换到左(右,下,上)的窗口。ctrl+w t(b)
:切换到最上(下)面的窗口。ctrl+w H(L,K,J)
: 将当前窗口移动到最左(右、上、下)面。ctrl+w r
:旋转窗口的位置。ctrl+w T
: 将当前的窗口移动到新的标签页上。~
: 反转光标所在字符的大小写。gu(U)
接范围(如$
,或 G
),可以把从光标当前位置到指定位置之间字母全部 转换成小写或大写。如ggguG
,就是把开头到最后一行之间的字母全部变为小 写。再如 gu5j
,把当前行和下面四行全部变成小写。r
: 替换光标处的字符,同样支持汉字。R
: 进入替换模式,按 esc
回到正常模式。[n] u
: 取消一(n)个改动。:undo 5
– 撤销 5 个改变。:undolist
– 你的撤销历史。ctrl + r
: 重做最后的改动。U
: 取消当前行中所有的改动。:earlier 4m
– 回到 4 分钟前:later 55s
– 前进 55 秒.
–重复上一个编辑动作qa
:开始录制宏 a(键盘操作记录)q
:停止录制@a
:播放宏 avim -x file
: 开始编辑一个加密的文件。:X
– 为当前文件设置密码。:set key=
– 去除文件的密码。这里是 滇狐总结的比较高级的 vi 技巧。
:e ++enc=utf8 filename
, 让 vim 用 utf-8 的编码打开这个文件。:w ++enc=gbk
,不管当前文件什么编码,把它转存成 gbk 编码。:set fenc 或 :set fileencoding
,查看当前文件的编码。set fileencoding=ucs-bom,utf-8,cp936
,vim 会根据要打开的文件选择合适的编码。 注意:编码之间不要留空格。 cp936 对应于 gbk 编码。 ucs-bom 对应于 windows 下的文件格式。让 vim 正确处理文件格式和文件编码,有赖于 ~/.vimrc的正确配置
大致有三种文件格式:unix, dos, mac. 三种格式的区别主要在于回车键的编码:dos 下是回车加换行,unix 下只有 换行符,mac 下只有回车符。
:e ++ff=dos filename
, 让 vim 用 dos 格式打开这个文件。:w ++ff=mac filename
, 以 mac 格式存储这个文件。:set ff
,显示当前文件的格式。set fileformats=unix,dos,mac
,让 vim 自动识别文件格式。gd
: 跳转到局部变量的定义处;gD
: 跳转到全局变量的定义处,从当前文件开头开始搜索;g;
: 上一个修改过的地方;g,
: 下一个修改过的地方;[[
: 跳转到上一个函数块开始,需要有单独一行的 {。]]
: 跳转到下一个函数块开始,需要有单独一行的 {。[]
: 跳转到上一个函数块结束,需要有单独一行的 }。][
: 跳转到下一个函数块结束,需要有单独一行的 }。[{
: 跳转到当前块开始处;]}
: 跳转到当前块结束处;[/
: 跳转到当前注释块开始处;]/
: 跳转到当前注释块结束处;%
: 不仅能移动到匹配的 (),{} 或 []
上,而且能在 #if,#else, #endif
之间跳跃。下面的括号匹配对编程很实用的。
ci', di', yi'
:修改、剪切或复制 '
之间的内容。ca', da', ya'
:修改、剪切或复制 '
之间的内容,包含 '
。ci", di", yi"
:修改、剪切或复制 "
之间的内容。ca", da", ya"
:修改、剪切或复制 "
之间的内容,包含 "
。ci(, di(, yi(
:修改、剪切或复制 ()
之间的内容。ca(, da(, ya(
:修改、剪切或复制 ()
之间的内容,包含 ()
。ci[, di[, yi[
:修改、剪切或复制 []
之间的内容。ca[, da[, ya[
:修改、剪切或复制 []
之间的内容,包含 []
。ci{, di{, yi{
:修改、剪切或复制 {}
之间的内容。ca{, da{, ya{
:修改、剪切或复制 {}
之间的内容,包含 {}
。ci<, di<, yi<
:修改、剪切或复制 <>
之间的内容。ca<, da<, ya<
:修改、剪切或复制 <>
之间的内容,包含<>
。Ctrl + ] | 找到光标所在位置的标签定义的地方 |
---|---|
Ctrl + t | 回到跳转之前的标签处 |
Ctrl + o | 退回原来的地方 |
[I | 查找全局标识符. Vim会列出它所找出的匹配行, 不仅在当前文件内查找,还会在所有的包含文件中查找 |
[i | 从当前文件起始位置开始查找第一处包含光标所指关键字的位置 |
]i | 类似上面的 [i ,但这里是从光标当前位置开始往下搜索 |
[{ | 转到上一个位于第一列的”{“。(前提是 “{” 和 “}” 都在第一列。) |
]} | 转到下一个位于第一列的”}” |
Ctrl+\+ s | 会出现所有调用、定义该函数的地方,输入索引号,回车即可 |
[ + ctrl + i | 跳转到函数、变量和 #define 用 ctrl+o 返回 |
[ + ctrl + d | 跳转到 #define 处用 ctrl+o 返回 |
ctags -R
: 生成 tag 文件,-R
表示也为子目录中的文件生成 tags:set tags=path/tags
– 告诉 ctags 使用哪个 tag 文件:tag xyz
– 跳到 xyz 的定义处,或者将光标放在 xyz 上按 C-]
,返回用 C-t
:stag xyz
– 用分割的窗口显示 xyz 的定义,或者 C-w ]
, 如果用 C-w n ]
,就会打开一个 n 行高的窗口:ptag xyz
– 在预览窗口中打开 xyz 的定义,热键是 C-w }
。:pclose
– 关闭预览窗口。热键是 C-w z
。:pedit abc.h
– 在预览窗口中编辑 abc.h:psearch abc
– 搜索当前文件和当前文件 include 的文件,显示包含 abc 的行。有时一个 tag 可能有多个匹配,如函数重载,一个函数名就会有多个匹配。 这种情况会先跳转到第一个匹配处。
:[n]tnext
– 下一 [n]
个匹配。:[n]tprev
– 上一 [n]
个匹配。:tfirst
– 第一个匹配:tlast
– 最后一个匹配:tselect tagname
– 打开选择列表tab 键补齐
:tag xyz<tab>
– 补齐以 xyz 开头的 tag 名,继续按 tab 键,会显示其他的。:tag /xyz<tab>
– 会用名字中含有 xyz 的 tag 名补全。ctags 对 c++ 生成 tags :
1 | ctags -R --c++-kinds=+p --fields=+iaS --extra=+q |
每个参数解释如下:
-R
: ctags 循环生成子目录的 tags
--c++-kinds=+px
: ctags 记录 c++ 文件中的函数声明和各种外部和前向声明
--fields=+iaS
: ctags 要求描述的信息
i
表示如果有继承,则标识出父类;a
表示如果元素是类成员的话,要标明其调用权限(即是 public 还是 private);S
表示如果是函数,则标识函数的 signature。--extra=+q
: 强制要求 ctags 做如下操作—如果某个语法元素是类的一个成员,ctags 默认会给其记录一行,可以要求 ctags 对同一个语法元斯屹记一行,这样可以保证在 VIM 中多个同名函数可以通过路径不同来区分。
查看阅读 c++ 代码
cscope 缺省只解析 C 文件 (.c
和 .h
)、lex 文件( .l
)和 yacc 文件( .y
),虽然它也可以支持 C++ 以及 Java,但它在扫描目录时会跳过 C++ 及 Java 后缀的文件。如果希望 cscope
解析 C++ 或 Java 文件,需要把这些文件的名字和路径保存在一个名为 cscope.files 的文件。当 cscope 发现在当前目录中存在 cscope.files 时,就会为 cscope.files 中列出的所有文件生成索引数据库。
下面的命令会查找当前目录及子目录中所有后缀名为 ".h", ".c", "cc"
和 ".cpp"
的文件,并把查找结果重定向到文件 cscope.files 中。然后 cscope 根据 cscope.files 中的所有文件,生成符号索引文件。最后一条命令使用 ctags 命令,生成一个 tags 文件,在 vim 中执行 ":help tags"
命令查询它的用法。它可以和 cscope 一起使用。
1 | find . -name "*.h" -o -name "*.c" -o -name "*.cc" -o "*.cpp" > cscope.files |
cscope -Rbq
: 生成 cscope.out 文件:cs add /path/to/cscope.out /your/work/dir
:cs find c func
– 查找 func 在哪些地方被调用:cw
– 打开 quickfix 窗口查看结果Gtags 综合了 ctags 和 cscope 的功能。 使用 Gtags 之前,你需要安装 GNU Gtags。 然后在工程目录运行 gtags 。
:Gtags funcname
定位到 funcname 的定义处。:Gtags -r funcname
查询 funcname被引用的地方。:Gtags -s symbol
定位 symbol 出现的地方。:Gtags -g string
Goto string 出现的地方。 :Gtags -gi string
忽略大小写。:Gtags -f filename
显示 filename 中的函数列表。 你可以用 :Gtags -f %
显示当前文件。:Gtags -P pattern
显示路径中包含特定模式的文件。 如 :Gtags -P .h$
显示所有头文件, :Gtags -P /vm/
显示 vm 目录下的文件。vim 提供了 :make
来编译程序,默认调用的是 make, 如果你当前目录下有 makefile,简单地 :make
即可。
如果你没有 make 程序,你可以通过配置 makeprg 选项来更改 make 调用的程序。 如果你只有一个 abc.java 文件,你可以这样设置:
1 | set makeprg=javac\ abc.java |
然后 :make
即可。如果程序有错,可以通过 quickfix 窗口查看错误。 不过如果要正确定位错误,需要设置好errorformat,让 vim 识别错误信息。 如:
1 | :setl efm=%A%f:%l:\ %m,%-Z%p^,%-C%.%# |
%f
表示文件名,%l
表示行号, %m
表示错误信息,其它的还不能理解。 请参考 :help errorformat
。
其实是 quickfix 插件提供的功能, 对编译调试程序非常有用
:copen
– 打开快速修改窗口。:cclose
– 关闭快速修改窗口。快速修改窗口在 make 程序时非常有用,当 make 之后:
:cl
– 在快速修改窗口中列出错误。:cn
– 定位到下一个错误。:cp
– 定位到上一个错误。:cr
– 定位到第一个错误。C-x C-s
– 拼写建议。C-x C-v
– 补全 vim 选项和命令。C-x C-l
– 整行补全。C-x C-f
– 自动补全文件路径。弹出菜单后,按 C-f
循环选择,当然也可以按 C-n 和 C-p
。C-x C-p 和C-x C-n
– 用文档中出现过的单词补全当前的词。 直接按 C-p 和 C-n
也可以。C-x C-o
– 编程时可以补全关键字和函数名啊。C-x C-i
– 根据头文件内关键字补全。C-x C-d
– 补全宏定义。C-x C-n
– 按缓冲区中出现过的关键字补全。 直接按 C-n 或 C-p
即可。当弹出补全菜单后:
C-p
向前切换成员;C-n
向后切换成员;C-e
退出下拉菜单,并退回到原来录入的文字;C-y
退出下拉菜单,并接受当前选项。>;
光标所在行会缩进。>;
,光标以下的 n 行会缩进。<;
,光标所在行会缩出。=
进行调整。=
,代码会按书写规则缩排好。n =
,调整 n 行代码的缩排。zf
– 创建折叠的命令,可以在一个可视区域上使用该命令;zd
– 删除当前行的折叠;zD
– 删除当前行的折叠;zfap
– 折叠光标所在的段;zo
– 打开折叠的文本;zc
– 收起折叠;za
– 打开/关闭当前折叠;zr
– 打开嵌套的折行;zm
– 收起嵌套的折行;zR (zO)
– 打开所有折行;zM (zC)
– 收起所有折行;zj
– 跳到下一个折叠处;zk
– 跳到上一个折叠处;zi -- enable/disable fold
;1 | vi ~/.zshrc |
:pwd
显示vim的工作目录。:cd path
改变 vim 的工作目录。:set autochdir
可以让 vim 根据编辑的文件自动切换工作目录。K
: 打开光标所在词的 manpage。*
: 向下搜索光标所在词。g*
: 同上,但部分符合即可。\#
: 向上搜索光标所在词。g#
: 同上,但部分符合即可。g C-g
: 统计全文或统计部分的字数。:h(elp) 或 F1
打开总的帮助。:help user-manual
打开用户手册。:
第一行指明怎么使用那个命令; 然后是缩进的一段解释这个命令的作用,然后是进一步的信息。:helptags somepath
为 somepath 中的文档生成索引。:helpgrep
可以搜索整个帮助文档,匹配的列表显示在 quickfix 窗口中。Ctrl+]
跳转到 tag 主题,Ctrl+t
跳回。:ver
显示版本信息。高亮所有搜索模式匹配
shift + *
向后搜索光标所在位置的单词
shift + #
向前搜索光标所在位置的单词
n 和 N 可以继续向后或者向前搜索匹配的字符串
:set hlsearch
高亮所有匹配的字符串
:nohlsearch
临时关闭
:set nohlsearch
彻底关闭,只有重新 :set hlsearch
才可以高亮搜索
vim 高亮显示光标所在的单词,在单词的地方输入 gd
语法高亮
syntax on
syntax off
vim自动补全
ctrl + n
或者 ctrl + p
复制 vim 文件中所有内容
gg
回到文件首
shift + v
进入 VISUAL LINE 模式
shift + g
全选所有内容
ctrl + insert
复制所选的内容
Vim 配置 Catgs 用于前端开发
Ubuntu16.04安装配置和使用ctags
universal-ctags
是一个现代化的ctag
实现,本文只介绍使用Vim的安装方法
1 | brew install --HEAD universal-ctags/universal-ctags/universal-ctags |
universal-ctags
是什么?A maintained ctags implementation, https://ctags.io, 一个负责的 ctags 实现,在github上开源并且持续更新和维护。
1 | sudo apt install autoconf |
把 ctags 可执行文件更新到系统 PATH 上?No, 我选择创建链接的方式:
1 | 如果你装了 emacs-snapshot,那么现在的 ctags 命令实际上链接到了 /usr/bin/ctags-snapshot,要先删除链接文件: |
其他系统请参考项目主页:ctags
每次生成ctags文件都要手动run一次命令,这一点也不Vim,当然也有解决方法。
vim-gutentags 是一个用于自动生成 tag 文件的插件。使用 vim-plug 安装
1 | Plug 'ludovicchabant/vim-gutentags' |
如果只是介绍安装方法,那就必要写这篇文章了,安装完成之后还是要针对前端开发的特点手动调教一下。首先我们在任何目录打开文件,都会在目录下生成 ctags 文件,这样的话对项目代码有入侵性,并不推荐,建议把 tag 文件写在特定的目录里。可以做如下设置:
1 | let g:gutentags_cache_dir = '~/.cachetags' |
这样生成的 tags 文件会统一放在 ~/.cachetags
目录下。另外默认生成的文件名叫 tags,也可以根据个从喜好修改:
1 | let g:gutentags_ctags_tagfile = '.tags' |
这样生成的文件是隐藏文件。另外一个很纠结的问题是有些文件我们并不想让他们生成 tags 文件,比如 node_modules
下文件,还有 .git
目录下的文件。这里有个取巧的方法是根据《Vim配置使用FZF》中的方法,把 ctags 获取文件列表的命令改成 ripgrep
的搜索,这样就可以自动忽略 .gitignore
下的文件。如下:
1 | let g:gutentags_file_list_command = 'rg --files' |
另外有的文件我们也不想让其生成 ctags 文件,比如 *.md
、*.svg
文件,可以通过 universal-ctags
的全局配置来配置,这里要注意的是 universal-ctags
默认的全局配置文件已经不是 ~/.ctags
和 ./.ctags
,而是在 ~/.ctags.d/*.ctags
和 ./.ctags.d/*.ctags
。比如我的全局配置文件放在~/.ctags.d/ignore.ctags
。简要配置如下 :
1 | --exclude=node_modules |
在文章《Vim配置使用FZF》中的介绍,FZF 是支持 ctags 的,所以可以做个快捷键配置,如下:
1 | map <leader>t :Tags<CR> |
这样就可以方便的使用 ctrl-]
和 ctrl-o
来进行 tag 跳转了。
Vim 8 中 C/C++ 符号索引:GTags
Ubuntu 安裝 GNU Global(gtags) 阅读Linux内核源码
]]>vim 入坑指南(六)插件 UltiSnips
在详细阐述网络传输过程之前,先来看一个最常见的例子,下图显示了一个网络服务器向客户端传送数据的完整过程:
消息要在网络中传输,必须对它进行编码,以特定的格式进行封装,同时需要适当地封装以足够的控制和地址信息,以使它能够从发送方移动到接收方。
消息大小
理论上,视频或邮件信息是能够以大块非中断型流从网络源地址传送到目的地址,但这也意味着同一时刻同一网络其他设备就无法收发消息。这种大型数据流会造成显著延时。并且,如果传输过程中连接断开,整个数据流都会丢失需要全部重传。因此更好的方法是将数据流分割(segmentation)为较小的,便于管理的片段,能够带来两点好处:
通过对片段打上标签的方式来保证顺序以及在接收时重组。
协议数据单元(Protocol Data Unit, PDU)
应用层数据在传输过程中沿着协议栈向下传递,每一层协议都会向其中添加首部信息,TCP首部和IP首部都是20字节的长度。这就是封装的过程。
数据片段在各层网络结构中采用的形式就称为协议数据单元(PDU)。封装过程中,下一层对从上一层收到的PDU进行封装。在处理的每一个阶段PDU都有不同的名字来反应它的功能。
PDU按照TCP/IP协议的命名规范:
封装
封装是指在传输之前为数据添加额外的协议头信息的过程。在绝大多数数据通信过程中,源数据在传输前都会封装以数层协议。在网络上发送消息时,主机上的协议栈从上至下进行操作。
以网络服务器为例,HTTP应用层协议发送HTML格式网页数据到传输层,应用层数据被分成TCP分段。各TCP分段被打上标签(主要是端口号,HTTP默认端口为80),称为首部(header),表明接收方哪一个进程应当接收此消息。同时也包含使得接收方能够按照原有的格式来重组数据的信息。
传输层将网页HTML数据封装成分段并发送至网络层,执行IP层协议。整个TCP分段封装成IP报文,也就是再添上IP首部。IP首部包括源和目的IP地址,以及发送报文到目的地址所必须的信息,包括一些控制字段。
之后,IP报文发送到链路层,封装以帧头和帧尾。每个帧头都包含源和目的物理地址。物理地址唯一指定了本地网络上的设备。帧尾包含差错校正信息。最后,由服务器网卡将比特编码传输给介质。
解封装
接收主机以相反的方式(从下至上)进行操作称为解封装。解封装是接收设备移除一层或多层协议头的过程。数据在协议栈中向上移动直到终端应用层伴随着解封装。
访问本地网络资源需要两种类型的地址:网络层地址和数据链路层地址。网络层和数据链路层负责将数据从发送设备传输至接收设备。两层协议都有源和目的地址,但两种地址的目的不同。
示例:客户端PC1与FTP在同一IP网络的通信
网络地址
网络层地址或IP地址包含两个部分:网络号和主机号。路由器使用网络前缀部分将报文转发给适当的网络。最后一个路由器使用主机部分将报文发送给目标设备。同一本地网络中,网络前缀部分是相同的,只有主机设备地址部分不同。
源IP地址:发送设备,即客户端PC1的IP地址:192.168.1.110
目的IP地址:接收设备,即FTP服务器:192.168.1.9
数据链路地址
数据链路地址(MAC)的目的是在同一网络中将数据链路帧从一个网络接口发送至另一个网络接口。以太网LAN和无线网LAN是两种不同物理介质的网络示例,分别有自己的数据链路协议。
当IP报文的发送方和接收方位于同一网络,数据链路帧直接发送到接收设备(通过ARP来获取目的IP的MAC地址)。以太网上数据链路地址就是以太网MAC地址。MAC地址是物理植入网卡的48比特地址。
源MAC地址:发送IP报文的PC1以太网卡MAC地址,AA-AA-AA-AA-AA-AA。
目的MAC地址:当发送设备与接收设备位于同一网络,即为接收设备的数据链路地址。本例中,FTP MAC地址:CC-CC-CC-CC-CC-CC。
源和目的MAC地址添加到以太网帧中。
MAC与IP地址
发送方必须知道接收方的物理和逻辑地址。发送方主机能够以多种方式学习到接收方的IP地址:比如浏览器缓存、getHostByName系统调用、域名系统(Domain Name System, DNS),或通过应用手动输入,如用户指定FTP地址。
以太网MAC地址是怎么识别的呢?发送方主机使用地址解析协议(Address Resolution Protocol, ARP)以检测本地网络的所有MAC地址。如下图所示,发送主机在整个LAN发送ARP请求消息,这是一条广播消息。ARP请求包含目标设备的IP地址,LAN上的每一个设备都会检查该ARP请求,看看是否包含它自身的IP地址。只有符合该IP地址的设备才会发送ARP响应。ARP响应包含ARP请求中IP地址相对应的MAC地址。
访问远程资源:
默认网关
当主机发送消息到远端网络,必须使用路由器,也称为默认网关。默认网关就是位于发送主机同一网络上的路由器的接口IP地址。有一点很重要:本地网络上的所有主机都能够配置自己的默认网关地址。如果该主机的TCP/IP设置中没有配置默认网关地址,或指定了错误的默认网关地址,则远端网络消息无法被送达。
如下图所示,LAN上的主机PC 1使用IP地址为192.168.1.1的R1作为默认网关,如果PDU的目的地址位于另一个网络,则主机将PDU发送至路由器上的默认网关。
与远端网络设备通讯
下图显示了客户端主机PC 1与远端IP网络服务器进行通讯的网络层地址与数据链路层地址:
网络地址
当报文的发送方与接收方位于不同网络,源和目的IP地址将会代表不同网络上的主机。
源IP地址:发送设备即客户端主机PC 1的IP地址:192.168.1.110。
目的IP地址:接收设备即网络服务器的IP地址:172.16.1.99。
数据链路地址
当报文的发送方与接收方位于不同网络,以太网数据链路帧无法直接被发送到目的主机。以太网帧必须先发送给路由器或默认网关。本例中,默认网关是R1,R1的接口IP地址与PC 1属于同一网络,因此PC 1能够直接达到路由器。
源MAC地址:发送设备即PC 1的MAC地址,PC1的以太网接口MAC地址为:AA-AA-AA-AA-AA-AA。
目的MAC地址:当报文的发送方与接收方位于不同网络,这一值为路由器或默认网关的以太网MAC地址。本例中,即R1的以太网接口MAC地址,即:11-11-11-11-11-11。
IP报文封装成的以太网帧先被传输至R1,R1再转发给目的地址即网络服务器。R1可以转发给另一个路由器,如果目的服务器所在网路连接至R1,则直接发送给服务器。
发送设备如何确定路由器的MAC地址?每一个设备通过自己的TCP/IP设置中的默认网关地址得知路由器的IP地址。之后,它通过ARP来得知默认网关的MAC地址,该MAC地址随后添加到帧中。
]]>图(graph)由顶点(vertex)和边(edge)的集合组成,每一条边就是一个点对(v,w)。
图的种类:地图,电路图,调度图,事物,网络,程序结构
图的属性:有V个顶点的图最多有V*(V-1)/2条边
邻接矩阵是一个元素为bool值的VV矩阵,若图中存在一条连接顶点V和W的边,折矩阵adj[v][w]=1,否则为0。占用的空间为VV,当图是稠密时,邻接矩阵是比较合适的表达方法。
对于非稠密的图,使用邻接矩阵有点浪费存储空间,可以使用邻接表,我们维护一个链表向量,给定一个顶点时,可以立即访问其链表,占用的空间为O(V+E)。
图的深度优先搜索(Depth First Search),和树的先序遍历比较类似。
它的思想:假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。 若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
显然,深度优先搜索是一个递归的过程。
下面以”无向图”为例,来对深度优先搜索进行演示。
对上面的图G1进行深度优先遍历,从顶点A开始。
第1步:访问A。
第2步:访问(A的邻接点)C。
在第1步访问A之后,接下来应该访问的是A的邻接点,即”C,D,F”中的一个。但在本文的实现中,顶点ABCDEFG是按照顺序存储,C在”D和F”的前面,因此,先访问C。
第3步:访问(C的邻接点)B。
在第2步访问C之后,接下来应该访问C的邻接点,即”B和D”中一个(A已经被访问过,就不算在内)。而由于B在D之前,先访问B。
第4步:访问(C的邻接点)D。
在第3步访问了C的邻接点B之后,B没有未被访问的邻接点;因此,返回到访问C的另一个邻接点D。
第5步:访问(A的邻接点)F。
前面已经访问了A,并且访问完了”A的邻接点B的所有邻接点(包括递归的邻接点在内)”;因此,此时返回到访问A的另一个邻接点F。
第6步:访问(F的邻接点)G。
第7步:访问(G的邻接点)E。
因此访问顺序是:A -> C -> B -> D -> F -> G -> E
下面以”有向图”为例,来对深度优先搜索进行演示。
对上面的图G2进行深度优先遍历,从顶点A开始。
第1步:访问A。
第2步:访问B。
在访问了A之后,接下来应该访问的是A的出边的另一个顶点,即顶点B。
第3步:访问C。
在访问了B之后,接下来应该访问的是B的出边的另一个顶点,即顶点C,E,F。在本文实现的图中,顶点ABCDEFG按照顺序存储,因此先访问C。
第4步:访问E。
接下来访问C的出边的另一个顶点,即顶点E。
第5步:访问D。
接下来访问E的出边的另一个顶点,即顶点B,D。顶点B已经被访问过,因此访问顶点D。
第6步:访问F。
接下应该回溯”访问A的出边的另一个顶点F”。
第7步:访问G。
因此访问顺序是:A -> B -> C -> E -> D -> F -> G
广度优先搜索算法(Breadth First Search),又称为”宽度优先搜索”或”横向优先搜索”,简称BFS。
它的思想是:从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。
换句话说,广度优先搜索遍历图的过程是以v为起点,由近至远,依次访问和v有路径相通且路径长度为1,2…的顶点。
下面以”无向图”为例,来对广度优先搜索进行演示。还是以上面的图G1为例进行说明。
第1步:访问A。
第2步:依次访问C,D,F。
在访问了A之后,接下来访问A的邻接点。前面已经说过,在本文实现中,顶点ABCDEFG按照顺序存储的,C在”D和F”的前面,因此,先访问C。再访问完C之后,再依次访问D,F。
第3步:依次访问B,G。
在第2步访问完C,D,F之后,再依次访问它们的邻接点。首先访问C的邻接点B,再访问F的邻接点G。
第4步:访问E。
在第3步访问完B,G之后,再依次访问它们的邻接点。只有G有邻接点E,因此访问G的邻接点E。
因此访问顺序是:A -> C -> D -> F -> B -> G -> E
下面以”有向图”为例,来对广度优先搜索进行演示。还是以上面的图G2为例进行说明。
第1步:访问A。
第2步:访问B。
第3步:依次访问C,E,F。
在访问了B之后,接下来访问B的出边的另一个顶点,即C,E,F。前面已经说过,在本文实现中,顶点ABCDEFG按照顺序存储的,因此会先访问C,再依次访问E,F。
第4步:依次访问D,G。
在访问完C,E,F之后,再依次访问它们的出边的另一个顶点。还是按照C,E,F的顺序访问,C的已经全部访问过了,那么就只剩下E,F;先访问E的邻接点D,再访问F的邻接点G。
因此访问顺序是:A -> B -> C -> E -> F -> D -> G
1 | /** |
1 | /** |
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径。
它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。
通过Dijkstra计算图G中的最短路径时,需要指定起点s(即从顶点s开始计算)。
此外,引进两个集合S和U。S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。
初始时,S中只有起点s;U中是除s之外的顶点,并且U中顶点的路径是”起点s到该顶点的路径”。然后,从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 然后,再从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 … 重复该操作,直到遍历完所有顶点。
(1)
初始时,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为”起点s到该顶点的距离”[例如,U中顶点v的距离为(s,v)的长度,然后s和v不相邻,则v的距离为∞]。
(2) 从U中选出”距离最短的顶点k”,并将顶点k加入到S中;同时,从U中移除顶点k。
(3)
更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;例如,(s,v)的距离可能大于(s,k)+(k,v)的距离。
(4) 重复步骤(2)和(3),直到遍历完所有顶点。
单纯的看上面的理论可能比较难以理解,下面通过实例来对该算法进行说明。
5.3迪杰斯特拉算法图解
以上图G4为例,来对迪杰斯特拉进行算法演示(以第4个顶点D为起点)。
初始状态:S是已计算出最短路径的顶点集合,U是未计算除最短路径的顶点的集合!
第1步:将顶点D加入到S中。
此时,S={D(0)}, U={A(∞),B(∞),C(3),E(4),F(∞),G(∞)}。 注:C(3)表示C到起点D的距离是3。
第2步:将顶点C加入到S中。
上一步操作之后,U中顶点C到起点D的距离最短;因此,将C加入到S中,同时更新U中顶点的距离。以顶点F为例,之前F到D的距离为∞;但是将C加入到S之后,F到D的距离为9=(F,C)+(C,D)。
此时,S={D(0),C(3)}, U={A(∞),B(23),E(4),F(9),G(∞)}。
第3步:将顶点E加入到S中。
上一步操作之后,U中顶点E到起点D的距离最短;因此,将E加入到S中,同时更新U中顶点的距离。还是以顶点F为例,之前F到D的距离为9;但是将E加入到S之后,F到D的距离为6=(F,E)+(E,D)。
此时,S={D(0),C(3),E(4)}, U={A(∞),B(23),F(6),G(12)}。
第4步:将顶点F加入到S中。
此时,S={D(0),C(3),E(4),F(6)}, U={A(22),B(13),G(12)}。
第5步:将顶点G加入到S中。
此时,S={D(0),C(3),E(4),F(6),G(12)}, U={A(22),B(13)}。
第6步:将顶点B加入到S中。
此时,S={D(0),C(3),E(4),F(6),G(12),B(13)}, U={A(22)}。
第7步:将顶点A加入到S中。
此时,S={D(0),C(3),E(4),F(6),G(12),B(13),A(22)}。
此时,起点D到各个顶点的最短距离就计算出来了:A(22) B(13) C(3) D(0) E(4) F(6) G(12)。
本文以”邻接矩阵”为例对迪杰斯特拉算法进行说明,
1 | // 邻接矩阵 |
Graph是邻接矩阵对应的结构体。
vexs用于保存顶点,vexnum是顶点数,edgnum是边数;matrix则是用于保存矩阵信息的二维数组。例如,matrix[i][j]=1,则表示”顶点i(即vexs[i])”和”顶点j(即vexs[j])”是邻接点;matrix[i][j]=0,则表示它们不是邻接点。
EData是邻接矩阵边对应的结构体。
1 | /* |
Git的第一个版本是Linux之父Linus Torvalds亲手操刀设计和实现的,Git 基于 DAG 结构 (Directed Acyclic Graph),其运行起来相当的快,它已经是现在的主流。
Git 和 SVN 思想最大的差别有四个:
去中心化
Git是一个DVCS(分布式版本管理系统),在技术层面上并不存在一个像中心仓库这样的东西 , 所有的数据都在本地,不存在谁是中心
图中每个开发者拉取(pull)并推送(push)到origin。但除了这种集中式的推送拉取关系,每个开发者也可能会从其他的开发者处拉取代码的变更,从技术上讲,这意味着Alice定义了一个名为bob的Git的remote,它指向了Bob的软件仓库。反之亦然。
直接记录快照,而非差异
Git每一个版本都是直接记录快照,而非文件的差异。 下面两个对比图在网上是广为流传大家应该熟悉:
SVN:
Git:
Git使用SHA-1算法计算数据的校验和,通过文件的内容或目录计算出SHA-1哈希值,作为指纹字符串,每个Version 都是一个快照。
不一样的分支概念
Git的分支本质是一个指向提交快照的指针,是从某个提交快照往回看的历史。当创建/切换分支的时候,只是变换了指针指向而已.而SVN创建一个分支, 是的的确确的复制了一份文件。
三个文件状态
在Git中文件有三种状态:
复制一个已创建的仓库:
1 | git clone ssh://user@domain.com/repo.git |
创建一个新的本地仓库:
1 | git init |
显示工作路径下已修改的文件:
1 | git status |
显示与上次提交版本文件的不同:
1 | git diff |
把当前所有修改添加到下次提交中:
1 | git add |
把对某个文件的修改添加到下次提交中:
1 | git add -p <file> |
提交本地的所有修改:
1 | git commit -a |
提交之前已标记的变化:
1 | git commit |
附加消息提交:
1 | git commit -m 'message here' |
提交,并将提交时间设置为之前的某个日期:
1 | git commit --date="`date --date='n day ago'`" -am "Commit Message" |
请勿修改已发布的提交记录!
1 | git commit --amend |
把当前分支中未提交的修改移动到其他分支
1 | git stash |
从当前目录的所有文件中查找文本内容:
1 | git grep "Hello" |
在某一版本中搜索文本:
1 | git grep "Hello" v2.5 |
从最新提交开始,显示所有的提交记录(显示hash, 作者信息,提交的标题和时间):
1 | git log |
显示所有提交(仅显示提交的hash和message):
1 | git log --oneline |
显示某个用户的所有提交:
1 | git log --author="username" |
显示某个文件的所有修改:
1 | git log -p <file> |
谁,在什么时间,修改了文件的什么内容:
1 | git blame <file> |
列出所有的分支:
1 | git branch |
切换分支:
1 | git checkout <branch> |
创建并切换到新分支:
1 | git checkout -b <branch> |
基于当前分支创建新分支:
1 | git branch <new-branch> |
基于远程分支创建新的可追溯的分支:
1 | git branch --track <new-branch> <remote-branch> |
删除本地分支:
1 | git branch -d <branch> |
给当前版本打标签:
1 | git tag <tag-name> |
列出当前配置的远程端:
1 | git remote -v |
显示远程端的信息:
1 | git remote show <remote> |
添加新的远程端:
1 | git remote add <remote> <url> |
下载远程端版本,但不合并到HEAD中:
1 | git fetch <remote> |
下载远程端版本,并自动与HEAD版本合并:
1 | git remote pull <remote> <url> |
将远程端版本合并到本地版本中:
1 | git pull origin master |
将本地版本发布到远程端:
1 | git push remote <remote> <branch> |
删除远程端分支:
1 | git push <remote> :<branch> (since Git v1.5.0) |
发布标签:
1 | git push --tags |
将分支合并到当前HEAD中:
1 | git merge <branch> |
将当前HEAD版本重置到分支中:
请勿重置已发布的提交!
1 | git rebase <branch> |
退出重置:
1 | git rebase --abort |
解决冲突后继续重置:
1 | git rebase --continue |
使用配置好的merge tool 解决冲突:
1 | git mergetool |
在编辑器中手动解决冲突后,标记文件为已解决冲突
1 | git add <resolved-file> |
放弃工作目录下的所有修改:
1 | git reset --hard HEAD |
移除缓存区的所有文件(i.e. 撤销上次git add):
1 | git reset HEAD |
放弃某个文件的所有本地修改:
1 | git checkout HEAD <file> |
重置一个提交(通过创建一个截然不同的新提交)
1 | git revert <commit> |
将HEAD重置到指定的版本,并抛弃该版本之后的所有修改:
1 | git reset --hard <commit> |
将HEAD重置到上一次提交的版本,并将之后的修改标记为未添加到缓存区的修改:
1 | git reset <commit> |
将HEAD重置到上一次提交的版本,并保留未提交的本地修改:
1 | git reset --keep <commit> |
1 | git remote add origin <git仓库地址> |
1 | 以下三种方式均可 |
1 | git remote rm origin |
在本地生成 ssh 私钥 / 公钥 文件
将「公钥」添加到 git 服务(github、gitlab、coding.net 等)网站后台
测试 git ssh 连接是否成功
接下来以添加 github ssh keys 为例,请注意替换 github 文件名。
注:如果对密钥机制不熟悉,建议不要指定 -f
参数,直接使用默认的 id_rsa 文件名。
1 | 运行以下命令,一直回车,文件名可随意指定 |
修改包含四种情况,需单独区分。
此类文件的状态为 Untracked files ,撤销方法如下:
1 | git clean -fd . |
其中,. 表示当前目录及所有子目录中的文件,也可以直接指定对应的文件路径,以下其他情况类似。
此类文件的状态为Changes not staged for commit
,撤销方法:
1 | git checkout . |
此类文件的状态为 Changes to be committed,撤销方法:
1 | git reset . |
执行之后文件将会回到以上的 1 或者 2 状态,可继续按以上步骤执行撤销,若 git reset 同时加上 –hard 参数,将会把修改过的文件也还原成版本库中的版本。
每次提交都会生成一个 hash 版本号,通过以下命令可查阅版本号并将其回滚:
1 | git log |
如果需要「回滚至上一次提交」,可直接使用以下命令:
1 | git reset head~1 |
执行之后,再按照 1 或者 2 状态进行处理即可,如果回滚之后的代码同时需要提交至 origin 仓库(即回滚 origin 线上仓库的代码),需要使用 -f 强制提交参数,且当前用户需要具备「强制提交的权限」。
如果是以上的情况 1 或者 2,只能歇屁了,因为修改没入过版本库,无法回滚。
如果是情况 4,回滚之后通过 git log 将看不到回滚之前的版本号,但可通过 git reflog 命令(所有使用过的版本号)找到回滚之前的版本号,然后 git reset <版本号> 。
两个分支进行合并时(通常是 git pull 时),可能会遇到冲突,同时被修改的文件会进入 Unmerged 状态,需要解决冲突。
大部分时候,「最快解决冲突」的办法是:使用当前 HEAD 的版本(ours),或使用合并进来的分支版本(theirs)。
1 | 使用当前分支 HEAD 版本,通常是冲突源文件的 <<<<<<< 标记部分,======= 的上方 |
用编辑器打开冲突的源文件进行修改,可能会发生遗留,且体验不好,通常需要借助 git mergetool 命令。
在 Mac 系统下,运行 git mergetool <文件名> 可以开启配置的第三方工具进行 merge,默认的是 FileMerge 应用程序,还可以配置成 Meld 或 kdiff3,体验更佳。
有三个好的习惯,可以减少代码的冲突:
在开始修改代码前先 git pull 一下;
将业务代码进行划分,尽量不要多个人在同一时间段修改同一文件;
通过Gitflow 工作流也可以提升 git流程效率,减少发生冲突的可能性。
如果你的项目周期比较长,还应该养成「定期 rebase 的习惯」,git pull –rebase 可以让分支的代码和 origin 仓库的代码保持兼容,同时还不会破坏线上代码的可靠性。
它的大概原理是,先将 origin 仓库的代码按 origin 的时间流在本地分支中提交,再将本地分支的修改记录追加到 origin 分支上。如果发生冲突,则可以即时的发现问题并解决,否则到项目上线时再解决冲突,可能会发生额外的风险。
rebase 大概的操作步骤如下:
1 | 将当前分支的版本追加到从远程 pull 回来的节点之后 |
有些修改没有完全完成之前,可能不需要提交到版本库,圡方法是将修改的文件 copy 到 git 仓库之外的目录临时存放,pull / merge 操作完成之后,再 copy 回来。
这样的做法一个是效率不高,另外一个可能会遗漏潜在的冲突。此类需求最好是通过 git stash 命令来完成,它可以将当前工作状态(WIP,work in progress)临时存放在 stash 队列中,待操作完成后再从 stash 队列中重新应用这些修改。
以下是 git stash 常用命令:
1 | 查看 stash 队列中已暂存了多少 WIP |
默认的 git log 会显示较全的信息,且不包含文件列表。使用 –name-status 可以看到修改的文件列表,使用 –oneline 可以将参数简化成一行。
1 | git log --name-status --oneline |
每次手动加上参数很麻烦,可以通过自定义快捷命令的方式来简化操作:
1 | git config --global alias.ls 'log --name-status --oneline --graph' |
运行以上配置后,可通过 git ls 命令来实现「自定义 git log」效果,通过该方法也可以创建 git st 、 git ci 等一系列命令,以便沿用 svn 命令行习惯。
1 | git config --global alias.st 'status --porcelain' |
更多 git log 参数,可通过 git help log 查看手册。
如果是看上一次提交的版本日志,直接运行 git show 即可。
此外,如果你的 Mac 安装了zsh(参考《全新Mac安装指南(编程篇),那么可以直接使用 gst、glog 等一系列快捷命令,详情见此列表:Plugin:git 。
例如,在执行 git submodule update 时有以下错误信息:
fatal: reference is not a tree: f869da471c5d8a185cd110bbe4842d6757b002f5
Unable to checkout ‘f869da471c5d8a185cd110bbe4842d6757b002f5’ in submodule path ‘source/i18n-php-server’
在此例中,发生以上错误是因为 i18n-php-server 子仓库在某电脑 A 的「本地」commit 了新的版本 「f869da471c5d8a185cd110bbe4842d6757b002f5」,且该次 commit 未 push origin。但其父级仓库 i18n-www 中引用了该子仓库的版本号,且将引用记录 push origin,导致其他客户机无法 update 。
解决方法,在电脑 A 上将 i18n-php-server 版本库 push origin 后,在其他客户机上执行 git submodule update 。或者用以上提到的 git reset 方法,将子仓库的引用版本号还原成 origin 上存在的最新版本号。
设置本地分支与远程分支保持同步,在第一次 git push 的时候带上 -u
参数即可
1 | git push origin master -u |
支持中文目录与文件名的显示(git 默认将非 ASCII 编码的目录与文件名以八进制编码展示)
1 | git config core.quotepath off |
常用的打 tag 操作,更多请查看《Git 基础 - 打标签》
1 | 列出所有本地 tag |
使用 git GUI 客户端(如,SoureTree、Github Desktop)能极大的提升分支管理效率。分支合并操作通常只有两种情况:从 origin merge 到本地,使用 git pull 即可;从另外一个本地分支 merge 到当前分支,使用 git merge <分支名>,以下是常用命令:
1 | 新建分支 branch1,并切换过去 |
沁原的硅谷创新课
Github项目推荐 | 基于 deepfakes(视频换脸)的非官方项目deepfakes_faceswap
]]>GitHub 项目地址:Dogs vs Cats (猫狗大战)
本项目是优达学城的一个毕业项目。项目要求使用深度学习方法识别一张图片是猫还是狗
项目使用Anaconda搭建环境。可是使用environment目录下的yml进行环境安装。
1 | conda env create -f environment.yml |
数据集来自 kaggle 上的一个竞赛:Dogs vs. Cats Redux: Kernels Edition。
下载kaggle猫狗数据集解压后分为 3 个文件 train.zip、 test.zip 和 sample_submission.csv。
train 训练集包含了 25000 张猫狗的图片, 每张图片包含图片本身和图片名。命名规则根据“type.num.jpg”方式命名。
test 测试集包含了 12500 张猫狗的图片, 每张图片命名规则根据“num.jpg”,需要注意的是测试集编号从 1 开始, 而训练集的编号从 0 开始。
sample_submission.csv 需要将最终测试集的测试结果写入.csv 文件中,上传至 kaggle 进行打分。
项目使用ResNet50, Xception, Inception V3 这三个模型完成。本项目的最低要求是 kaggle Public Leaderboard 前10%。在kaggle上,总共有1314只队伍参加了比赛,所以需要最终的结果排在131位之前,131位的得分是0.06127,所以目标是模型预测结果要小于0.06127。
kaggle 官方的评估标准是 LogLoss,下面的表达式就是二分类问题的 LogLoss 定义。
其中:
n 是测试集中图片数量
是图片预测为狗的概率
如果图像是狗,则为1,如果是猫,则为0
是自然(基数 )对数
对数损失越小,代表模型的性能越好。上述评估指标可用于评估该项目的解决方案以及基准模型。
1 | cd model_graphviz/ |
整个模型是在本地训练的,训练了三天才完成。建议使用云端 GPU 训练复现实验过程。
1. 数据预处理
2. 模型搭建
Kera的应用模块Application提供了带有预训练权重的Keras模型,这些模型可以用来进行预测、特征提取和微调整和。
299*299*3
299*299*3
224*224*3
在Keras中载入模型并进行全局平均池化,只需要在载入模型的时候,设置include_top=False
, pooling='avg'
. 每个模型都将图片处理成一个1*2048
的行向量,将这三个行向量进行拼接,得到一个1*6144
的行向量, 作为数据预处理的结果。
3. 模型训练&模型调参
载入预处理的数据之后,先进行一次概率为0.5的dropout,然后直接连接输出层,激活函数为Sigmoid,优化器为Adam,输出一个零维张量,表示某张图片中有狗的概率。
4. 模型评估
5. 可视化
项目使用 Keras 和 Flask 搭建部署一个简单易用的深度学习图像网页应用,可以通过网页导入一张彩色猫或者狗的图片预测是猫或者狗的概率。
项目目录结构:
1 | . |
1 | conda env create -f environmert.yml |
1 | python app.py |
这时候用浏览器打开 http://localhost:5000/ 就可以进行网页导入图片预测图片是狗的概率了。
如果不想搭建环境复现实验结果,可以按照以下操作分分钟复现实验结果:
1 | docker pull miaowmiaow/webapp:1.1.0 |
到此就可以在浏览器中输入 http://localhost:5000 就可以使用网页对导入的猫狗图片做预测了。
下图为预测的效果图:
]]>WebSocket 是一种网络通信协议,很多高级功能都需要它。
初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用“轮询”:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。
轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。
WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
其他特点包括:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是 ws
(如果加密,则为 wss
),服务器网址就是 URL。
1 | ws://example.com:80/some/path |
WebSocket 的用法相当简单。
下面是一个网页脚本的例子(点击这里看运行结果),基本上一眼就能明白。
1 | var ws = new WebSocket("wss://echo.websocket.org"); |
WebSocket 客户端的 API 如下。
WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例。
1 | var ws = new WebSocket('ws://localhost:8080'); |
执行上面语句之后,客户端就会与服务器进行连接。
实例对象的所有属性和方法清单,参见这里。
readyState
属性返回实例对象的当前状态,共有四种。
下面是一个示例。
1 | switch (ws.readyState) { |
实例对象的 onopen
属性,用于指定连接成功后的回调函数。
1 | ws.onopen = function () { |
如果要指定多个回调函数,可以使用addEventListener`方法。
1 | ws.addEventListener('open', function (event) { |
实例对象的onclose
属性,用于指定连接关闭后的回调函数。
1 | ws.onclose = function(event) { |
实例对象的 onmessage
属性,用于指定收到服务器数据后的回调函数。
1 | ws.onmessage = function(event) { |
注意,服务器数据可能是文本,也可能是二进制数据( blob
对象或 Arraybuffer
对象)。
1 | ws.onmessage = function(event){ |
除了动态判断收到的数据类型,也可以使用binaryType
属性,显式指定收到的二进制数据类型。
1 | // 收到的是 blob 数据 |
实例对象的 send( )
方法用于向服务器发送数据。
发送文本的例子。
1 | ws.send('your message'); |
发送 Blob 对象的例子。
1 | var file = document |
发送 ArrayBuffer 对象的例子。
1 | // Sending canvas ImageData as ArrayBuffer |
实例对象的 bufferedAmount
属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。
1 | var data = new ArrayBuffer(10000000); |
实例对象的onerror
属性,用于指定报错时的回调函数。
1 | socket.onerror = function(event) { |
WebSocket 服务器的实现,可以查看维基百科的列表。
常用的 Node 实现有以下三种。
具体的用法请查看它们的文档,这里不详细介绍了。
下面,我要推荐一款非常特别的 WebSocket 服务器:Websocketd。
它的最大特点,就是后台脚本不限语言,标准输入(stdin)就是 WebSocket 的输入,标准输出(stdout)就是 WebSocket 的输出。
举例来说,下面是一个 Bash 脚本 counter.sh
。
1 |
|
命令行下运行这个脚本,会输出1、2、3,每个值之间间隔1秒。
1 | $ bash ./counter.sh |
现在,启动websocketd
,指定这个脚本作为服务。
1 | $ websocketd --port=8080 bash ./counter.sh |
上面的命令会启动一个 WebSocket 服务器,端口是 8080
。每当客户端连接这个服务器,就会执行 counter.sh
脚本,并将它的输出推送给客户端。
1 | var ws = new WebSocket('ws://localhost:8080/'); |
上面是客户端的 JavaScript 代码,运行之后会在控制台依次输出1、2、3。
有了它,就可以很方便地将命令行的输出,发给浏览器。
1 | $ websocketd --port=8080 ls |
上面的命令会执行ls
命令,从而将当前目录的内容,发给浏览器。使用这种方式实时监控服务器,简直是轻而易举(代码)。
更多的用法可以参考官方示例。
websocketd 的实质,就是命令行的 WebSocket 代理。只要命令行可以执行的程序,都可以通过它与浏览器进行 WebSocket 通信。下面是一个 Node 实现的回声服务 greeter.js
。
1 | process.stdin.setEncoding('utf8'); |
启动这个脚本的命令如下。
1 | $ websocketd --port=8080 node ./greeter.js |
官方仓库还有其他各种语言的例子。
硬件环境:
软件环境:
CLI命令如下;
1 | $ mount -o loop ubuntu-16.04.2-server-amd64.iso /mnt/temp |
相关配置文件(menu.cfg、txt.cfg、isolinux.cfg此文件不是必须要修改,具体见下边解释)。将光盘文件,拷贝到临时目录(家目录或者自己新建目录均可,但建议拷贝到/var或/temp目录下),具体命令如下:
1 | $ cp -rf /mnt/temp/ /var/mycdrom |
因为 /mnt
目录的默认权限是 333
,所以在此使用 -r
和 -f
参数,-r
代表递归,即文件夹下所有文件都拷贝,-f
代表强制执行;
更改 menu.cfg
文件,如下图,主要是注释掉标准安装的配置文件,以便可以定制安装。
1 | $ cd /var/mycdrom/temp/isolinux |
注:
vi有三种模式,普通模式、编辑模式、命令行模式;
I o a进入编辑模式,
普通模式下数字+yy复制
P黏贴
命令行模式:w写入,q离开,!强制执行
注释 menu.cfg
内容如下红框所示:
更改 txt.cfg
文件,主要用于定制串口安装(如下图):
更改 isolinux.cfg
文件,主要修改grub菜单等待时间(如下图),也可不修改;
命令如下:
1 | $ genisoimage -o ubuntu-16.04.2-server-adm64-console_115200.iso -r -J -no-emul-boot -boot-load-size 4 -boot-info-table -b isolinux/isolinux.bin -c isolinux/boot.cat /var/mycdrom/temp |
genisoimage
是linux各大发行版制作ISO镜像比较流行的工具,若要定制系统,最好在linux下更改相关配置,并使用此工具重新打包;若在Windows平台使用UltraISO等工具解压更改重新打包会出现不稳定的情况(无法找到镜像,无法找到安装源等)。
-o
:是output缩写,用来指定输出镜像名称-r
: 即rational-rock,用来开放ISO文件所有权限(r、w、x) -J
: 即Joliet,一种ISO9600扩展格式,用来增加兼容性,最好加上-no-emul-boot
-boot-load-size 4
-boot-info-table
:指定兼容模式下虚拟扇区的数量,若不指定,有些BISO会出现一些问题-b
:指定开机映像文件-c
:具体开机配置文件Reboot系统U盘启动,即可安装系统。
关闭系统插入U盘,启动系统,看到如下提示按F12进入安装系统模式:
1 | Press F12 for boot menu.. |
选择U盘所在的选项。
若是硬盘已有linux发行版系统,那在如下界面,必须umount分区,才能将更改写入分区表
如下界面,若有特许需求(需要安装一些特许软件apache、weblogic等)可以选择自动更新(需要联网),一般情况不选则自动更新
]]>1 | $ echo 'Hello world !' |
打印彩色输出:
1 | # 彩色文本 |
1 | $ printf "%-5s %-10s %-4s\n" No Name Mark |
原理:
%-5s
指明了一个格式为左对齐且宽度为5的字符串替换( -
表示左对齐)。如果不用 -
指定对齐方式,字符串就采用右对齐形式。
%s
、 %c
、%d
和 %f
都是格式替换符(format substitution character),其所对应的参数可以置于带引号的格式字符串之后。
1 | # tr 是 translate的简写 |
1 | $ var="value" |
1 | # 在PATH中添加一条新路径 |
1 | # 单引号 |
1 | # 用法 |
1 | $ echo $SHELL |
在Bash shell环境中,可以利用 let
、(( ))
和[]
执行基本的算术操作。而在进行高级操作时,expr
和 bc
这两个工具也会非常有用。
使用 let
时,变量名之前不需要再添加 $
1 | $ no1=4 |
1 | # 操作符[]的使用方法和let命令类似 |
1 | # 使用(())时,变量名之前需要加上$ |
1 | # expr同样可以用于基本算术操作 |
bc是一个用于数学运算的高级工具,这个精密计算器包含了大量的选项 。此处不多介绍。
1、单小括号 ( )
cmd
,shell扫描一遍命令行,发现了$(cmd)结构
,便将 $(cmd)
中的cmd执行一次,得到其标准输出,再将此输出放到原来命令。有些shell不支持,如tcsh。2、双小括号 (( ))
$((exp))
中,甚至是三目运算符。作不同进位(如二进制、八进制、十六进制)运算时,输出结果全都自动转化成了十进制。如:echo $((16#5f)) 结果为95 (16进位转十进制)。$
符号前缀。括号内支持多个表达式用逗号分开。 只要括号中的表达式符合C语言运算规则,比如可以直接使用for((i=0;i<5;i++)), 如果不使用双括号, 则为for i in seq 0 4
或者for i in {0..4}。再如可以直接使用 if (($i<5))
, 如果不使用双括号, 则为 if [ $i -lt 5 ]
。1、单中括号 []
2、双中括号 [[ ]]
if [[ $a != 1 && $a != 2 ]]
, 如果不适用双括号, 则为 if [ $a -ne 1] && [ $a != 2 ]
或者 if [ $a -ne 1 -a $a != 2 ]
。1)常规用法
2)几种特殊的替换结构
1 | ${var:-string},${var:+string},${var:=string},${var:?string} |
${var:-string}
和 ${var:=string}:
若变量var为空,则用在命令行中用string来替换 ${var:-string}
,否则变量var不为空时,则用变量var的值来替换 ${var:-string}
;对于 ${var:=string}
的替换规则和 ${var:-string}
是一样的,所不同之处是 ${var:=string}
若var为空时,用string替换 ${var:=string}
的同时,把string赋给变量 var: ${var:=string}
很常用的一种用法是,判断某个变量是否赋值,没有的话则给它赋上一个默认值。${var:+string}
的替换规则和上面的相反,即只有当var不是空的时候才替换成string,若var为空时则不替换或者说是替换成变量 var的值,即空值。(因为变量var此时为空,所以这两种说法是等价的) 。${var:?string}
替换规则为:若变量var不为空,则用变量var的值来替换 ${var:?string}
;若变量var为空,则把string输出到标准错误中,并从脚本中退出。我们可利用此特性来检查是否设置了变量的值。补充扩展:在上面这五种替换结构中string不一定是常值的,可用另外一个变量的值或是一种命令的输出。
3)四种模式匹配替换结构
模式匹配记忆方法:
1 | # 是去掉左边(在键盘上#在$之左边) |
1 | ${var%pattern},${var%%pattern},${var#pattern},${var##pattern} |
${variable%pattern}
,这种模式时,shell在variable中查找,看它是否一给的模式pattern结尾,如果是,就从命令行把variable中的内容去掉右边最短的匹配模式${variable%%pattern}
,这种模式时,shell在variable中查找,看它是否一给的模式pattern结尾,如果是,就从命令行把variable中的内容去掉右边最长的匹配模式${variable#pattern}
这种模式时,shell在variable中查找,看它是否一给的模式pattern开始,如果是,就从命令行把variable中的内容去掉左边最短的匹配模式${variable##pattern}
这种模式时,shell在variable中查找,看它是否一给的模式pattern结尾,如果是,就从命令行把variable中的内容去掉右边最长的匹配模式这四种模式中都不会改变variable的值,其中,只有在pattern中使用了匹配符号时,%和%%,#和##才有区别。结构中的pattern支持通配符,表示零个或多个任意字符,?表示仅与一个任意字符匹配,[…]表示匹配中括号里面的字符,[!…]表示不匹配中括号里面的字符。
4)字符串提取和替换
1 | ${var:num},${var:num1:num2},${var/pattern/pattern},${var//pattern/pattern} |
${var:num}
,这种模式时,shell在var中提取第num个字符到末尾的所有字符。若num为正数,从左边0处开始;若num为负数,从右边开始提取字串,但必须使用在冒号后面加空格或一个数字或整个num加上括号,如 ${var: -2}
、${var:1-3}
或 ${var:(-2)}
。 ${var:num1:num2}
,num1是位置,num2是长度。表示从 $var字符串的第$num1
个位置开始提取长度为$num2的子串。不能为负数。${var/pattern/pattern}
表示将var字符串的第一个匹配的pattern替换为另一个pattern。。 ${var//pattern/pattern}
表示将var字符串中的所有能匹配的pattern替换为另一个pattern。${a}
变量a的值, 在不引起歧义的情况下可以省略大括号。$(cmd)
命令替换,和cmd
效果相同,结果为shell命令cmd的输,过某些Shell版本不支持 $()
形式的命令替换, 如tcsh。$((expression))
和exprexpression
效果相同, 计算数学表达式exp的数值, 其中exp只要符合C语言的运算规则即可, 甚至三目运算符和逻辑表达式都可以计算。(cmd1;cmd2;cmd3)
新开一个子shell顺序执行命令cmd1,cmd2,cmd3, 各命令之间用分号隔开, 最后一个命令后可以没有分号。{ cmd1;cmd2;cmd3;}
在当前shell顺序执行命令cmd1,cmd2,cmd3, 各命令之间用分号隔开, 最后一个命令后必须有分号, 第一条命令和左括号之间必须用空格隔开。对 {}
和 ()
而言, 括号中的重定向符只影响该条命令,而括号外的重定向符影响到括号中的所有命令。
变量 | 含义 |
---|---|
$0 | 当前脚本的文件名。 |
$n | 传递给脚本或函数的参数。n是一个数字,表示几个参数。 |
$# | 传递给脚本或函数的参数个数。 |
$* | 传递给脚本或函数的所有参数。 |
$@ | 传递给脚本或函数的所有采纳数。被双引号(“ “)包含是,与$* 稍有不同。 |
$? | 上个命令的退出状态,或函数的返回值。 |
$$ | 当前shell进程ID。对于shell脚本,就是这个脚本所在的进程ID。 |
运行脚本时传递给脚本的参数称为命令行参数。命令行参数用 $n
表示,例如,$1
表示第一个参数,$2
表示第二个参数,依次类推。
$*
和 $@
的区别$*
和 $@
都表示传递给函数或脚本的所有参数,不被双引号(“ “)包含时,都以"$1" "$2" … "$n"
的形式输出所有参数。
但是当它们被双引号(“ “)包含时,"$*"
会将所有的参数作为一个整体,以"$1 $2 … $n"
的形式输出所有参数;"$@"
会将各个参数分开,以 "$1" "$2" … "$n"
的形式输出所有参数。
$?
可以获取上一个命令的退出状态。所谓退出状态,就是上一个命令执行后的返回结果。
退出状态是一个数字,一般情况下,大部分命令执行成功会返回 0,失败返回 1。
不过,也有一些命令返回其他值,表示不同类型的错误。
$?
也可以表示函数的返回值,此处不展开。
1、重定向符号
1 | > 输出重定向到一个文件或设备 覆盖原来的文件 |
2、标准输入刷出
1 | 在 bash 命令执行的过程中,主要有三种输出入的状况,分别是: |
3、使用实例
1 | # & 是一个描述符,如果1或2前不加&,会被当成一个普通文件。 |
要在终端中打印stdout,同时将它重定向到一个文件中,那么可以这样使用tee 。
1 | # 用法:command | tee FILE1 FILE2 |
数组是Shell脚本非常重要的组成部分,它借助索引将多个独立的独立的数据存储为一个集合。普通数组只能使用整数作为数组索引,关联数组不仅可以使用整数作为索引,也可以使用字符串作为索引。通常情况下,使用字符串做索引更容易被人们理解。Bash从4.0之后开始引入关联数组。
数组的方法有如下几种:
1 | #在一行上列出所有元素 |
注意:第一种方法要使用圆括号,否则后面会报错。
数组元素的方法有如下几种:
1 | $ echo ${array_var[0]} #输出结果为 test1 |
注意:在ubuntu 14.04中,shell脚本要以#!/bin/bash开头,且执行脚本的方式为 bash test.sh。
定义关联数组
在关联数组中,可以使用任何文本作为数组索引。定义关联数组时,首先需要使用声明语句将一个变量声明为关联数组,然后才可以在数组中添加元素,过程如下:
1 | $ declare -A ass_array #声明一个关联数组 |
注意:对于普通数组,使用上面的方法依然可以列出索引列表,在声明关联数组以及添加数组元素时,都不能在前面添加美元符$。
alias命令的作用只是暂时的。一旦关闭当前终端,所有设置过的别名就失效了。为了使别名设置一直保持作用,可以将它放入~/.bashrc文件中。因为每当一个新的shell进程生成时,都会执行 ~/.bashrc中的命令。
1 | $ alias install='sudo apt-get install' |
时间方面 :
1 | % : 印出 |
日期方面 :
1 | %a : 星期几 (Sun..Sat) |
若是不以加号作为开头,则表示要设定时间,而时间格式为 MMDDhhmm[[CC]YY][.ss]
,其中:
1 | MM 为月份, |
参数 :
-d datestr : 显示 datestr 中所设定的时间 (非系统时间)
–help : 显示辅助讯息
-s datestr : 将系统时间设为 datestr 中所设定的时间
-u : 显示目前的格林威治时间
–version : 显示版本编号
例子:
1 | $ date# 获取日期 |
1 | $ bash -x script.sh |
1 | #!/bin/bash |
1 | #!/bin/bash |
可以将调试功能置为”on”来运行上面的脚本:
1 | $ _DEBUG=on ./script.sh |
我们在每一个需要打印调试信息的语句前加上DEBUG。如果没有把 _DEBUG=on传递给脚本,那么调试信息就不会打印出来。在Bash中,命令 :
告诉shell不要进行任何操作。
shebang的妙用
把shebang从 #!/bin/bash
改成 #!/bin/bash -xv
,这样一来,不用任何其他选项就可以启用调试功能了。
1 | $0 # 脚本名 |
导出函数:
函数也能像环境变量一样用export导出,如此一来,函数的作用域就可以扩展到子进程中,例如:
1 | export -f fname |
1 | # 从输入中读取n个字符并存入变量 |
1 | # if条件 |
if的条件判断部分可能会变得很长,但可以用逻辑运算符将它变得简洁一些:
1 | [ condition ] && action # 如果condition为真,则执行action |
&&
是逻辑与运算符, ||
是逻辑或运算符。编写Bash脚本时,这是一个很有用的技巧。现在来了解一下条件和比较操作。
算术比较:
-gt
:大于。 -lt
:小于。 -ge
:大于或等于。 -le
:小于或等于。 可以按照下面的方法结合多个条件进行测试:
1 | [ $var1 -ne 0 -a $var2 -gt 2 ] #使用逻辑与-a |
文件系统相关测试:
我们可以使用不同的条件标志测试不同的文件系统相关的属性。
[ -f $file_var ]
:如果给定的变量包含正常的文件路径或文件名,则返回真。 [ -x $var ]
:如果给定的变量包含的文件可执行,则返回真。 [ -d $var ]
:如果给定的变量包含的是目录,则返回真。 [ -e $var ]
:如果给定的变量包含的文件存在,则返回真。 [ -c $var ]
:如果给定的变量包含的是一个字符设备文件的路径,则返回真。 [ -b $var ]
:如果给定的变量包含的是一个块设备文件的路径,则返回真。 [ -w $var ]
:如果给定的变量包含的文件可写,则返回真。 [ -r $var ]
:如果给定的变量包含的文件可读,则返回真。 [ -L $var ]
:如果给定的变量包含的是一个符号链接,则返回真。 字符串比较:
使用字符串比较时,最好用双中括号,因为有时候采用单个中括号会产生错误,所以最好避开它们。
可以用下面的方法检查两个字符串,看看它们是否相同。
[[ $str1 = $str2 ]]
:当str1等于str2时,返回真。也就是说, str1和str2包含[[ $str1 == $str2 ]]
:这是检查字符串是否相等的另一种写法。 也可以检查两个字符串是否不同。
[[ $str1 != $str2 ]]
:如果str1和str2不相同,则返回真。 我们还可以检查字符串的字母序情况,具体如下所示。
[[ $str1 > $str2 ]]
:如果str1的字母序比str2大,则返回真。 [[ $str1 < $str2 ]]
:如果str1的字母序比str2小,则返回真。 [[ -z $str1 ]]
:如果str1包含的是空字符串,则返回真。 [[ -n $str1 ]]
:如果str1包含的是非空字符串,则返回真。 使用逻辑运算符 && 和 || 能够很容易地将多个条件组合起来:
1 | if [[ -n $str1 ]] && [[ -z $str2 ]] |
test命令可以用来执行条件检测。用test可以避免使用过多的括号。之前讲过的[]中的测试条件同样可以用于test命令。
1 | if [ $var -eq 0 ]; then echo "True"; fi |
子shell本身就是独立的进程。可以使用 ( )
操作符来定义一个子shell :
1 | pwd; |
1 | repeat() { while true; do $@ && return; done } |
工作原理:
函数repeat,它包含了一个无限while循环,该循环执行以参数形式(通过 $@
访问)传入函数的命令。如果命令执行成功,则返回,进而退出循环。
一种更快的做法 :
在大多数现代系统中, true
是作为 /bin
中的一个二进制文件来实现的。
这就意味着每执行一次while循环, shell就不得不生成一个进程。
如果不想这样,可以使用shell内建的:
命令,它总是会返回为0的退出码: 1 | repeat() { while :; do $@ && return; done } |
尽管可读性不高,但是肯定比第一种方法快。
1 | # 摆脱多余的空白行 |
1 | # 列出当前目录及子目录下所有的文件和文件夹 |
1、find命令有一个选项 -iname
(忽略字母大小写)
1 | $ ls |
2、如果想匹配多个条件中的一个,可以采用OR条件操作 :
1 | $ ls |
3、选项-path的参数可以使用通配符来匹配文件路径。 -name
总是用给定的文件名进行匹配。-path
则将文件路径作为一个整体进行匹配。例如 :
1 | $ find /home/users -path "*/slynux/*" -print |
4、选项 -regex
的参数和 -path
的类似,只不过 -regex
是基于正则表达式来匹配文件路径的。
1 | $ ls |
5、find也可以用“!”否定参数的含义。例如:
1 | $ ls |
6、基于目录深度的搜索
1 | # 深度选项-maxdepth和 -mindepth来限制find命令遍历的目录深度 |
注:-maxdepth和-mindepth应该作为find的第三个参数出现。如果作为第4个或之后的参数,就可能会影响到find的效率,因为它不得不进行一些不必要的检查。
根据文件类型搜索
7、根据文件类型搜索
-type
可以对文件搜索进行过滤
文件类型 | 类型参数 |
---|---|
普通文件 | f |
符号链接 | l |
目录 | d |
字符设备 | c |
块设备 | b |
套接字 | s |
FIFO | p |
8、根据文件时间进行搜索
-atime、 -mtime、 -ctime可作为find的时间选项。它们可以用整数值指定,单位是天。这些整数值通常还带有 - 或 + : - 表示小于, + 表示大于。
1 | # 打印出在最近7天内被访问过的所有文件: |
-atime、 -mtime以及-ctime都是基于时间的参数,其计量单位是“天”。还有其他一些基于时间的参数是以分钟作为计量单位的。这些参数包括:
使用 -newer
,我们可以指定一个用于比较时间戳的参考文件,然后找出比参考文件更新的(更近的修改时间)所有文件
1 | # 找出比file.txt修改时间更近的所有文件: |
9、基于文件大小的搜索
1 | $ find . -type f -size +2k |
10、删除匹配的文件
-delete
可以用来删除find查找到的匹配文件。
1 | # 删除当前目录下所有的 .swp文件: |
11、基于文件权限和所有权的匹配
1 | $ find . -type f -perm 644 -print |
-perm指明find应该只匹配具有特定权限值的文件。
12、利用find执行命令或动作
find命令可以借助选项-exec与其他命名进行结合。 -exec算得上是find最强大的特性之一。
1 | $ find . -type f -user root -exec chown slynux {} \; |
-exec
结合多个命令 :
我们无法在-exec参数中直接使用多个命令。它只能够接受单个命令,不过我们可以耍一个小花招。把多个命令写到一个shell脚本中(例如command.sh),然后在-exec中使用这个脚本:
1 | -exec ./commands.sh {} \; |
13、让find跳过特定的目录
1 | $ find devel/source_path \( -name ".git" -prune \) -o \( -type f -print \) |
\( -name ".git" -prune \)
的作用是用于进行排除,它指明了 .git目录应该被排除在外,而\( -type f -print \)
指明了需要执行的动作。这些动作需要被放置在第二个语句块中(打印出所有文件的名称和路径)。
xargs
擅长将标准输入数据转换成命令行参数。
xargs
命令把从 stdin接收到的数据重新格式化,再将其作为参数提供给其他命令。
只需要将换行符移除,再用” “(空格)进行代替,就可以实现多行输入的转换。
1 | $ cat example.txt # 样例文件 |
指定每行最大的参数数量 n
,我们可以将任何来自stdin的文本划分成多行,每行 n
个参数。
1 | $ cat example.txt | xargs -n 3 |
用 -d
选项为输入指定一个定制的定界符:
1 | $ echo "splitXsplitXsplitXsplit" | xargs -d X |
在这里,我们明确指定X作为输入定界符,而在默认情况下, xargs采用内部字段分隔符(空格)作为输入定界符。
-I
指定替换字符串,这个字符串在xargs扩展时会被替换掉。如果将 -I
与 xargs
结合使用,对于每一个参数,命令都会被执行一次。
1 | $ cat args.txt |
-I {}
指定了替换字符串。对于每一个命令参数,字符串 {}
都会被从stdin读取到的参数替换掉。
使用 -I
的时候,命令以循环的方式执行。
xargs和find算是一对死党。两者结合使用可以让任务变得更轻松。 不过人们通常却是以一种错误的组合方式使用它们。例如:
1 | $ find . -type f -name "*.txt" -print | xargs rm -f |
这样做很危险。 有时可能会删除不必要删除的文件。
只要我们把 find
的输出作为 xargs
的输入,就必须将 -print0
与 find
结合使用,以字符null('\0')
来分隔输出。
1 | $ find . -type f -name "*.txt" -print0 | xargs -0 rm -f |
校验和(checksum)程序用来从文件中生成校验和密钥,然后利用这个校验和密钥核实文件的完整性。文件可以通过网络或任何存储介质分发到不同的地点。
最知名且使用最为广泛的校验和技术是md5sum和SHA-1。它们对文件内容使用相应的算法来生成校验和。
1 | $ md5sum filename |
计算SAH-1串的命令是sha1sum。其用法和md5sum的非常相似。只需要把先前讲过的那些命令中的md5sum替换成sha1sum就行了,记住将输入文件名从file_sum.md5改为file_sum.sha1。
对目录进行校验:
1 | $ md5deep -rl directory_path > directory.md5 |
crypt
、 gpg
、 base64
、 md5sum
、 sha1sum
以及 openssl
的用法。
1)crypt是一个简单的加密工具,它从stdin接受一个文件以及口令作为输入,然后将加密数据输出到Stdout(因此要对输入、输出文件使用重定向)。
1 | $ crypt <input_file >output_file |
2)gpg(GNU隐私保护)是一种应用广泛的工具,它使用加密技术来保护文件,以确保数据在送达目的地之前无法被读取。这里我们讨论如何加密、解密文件。
1 | # 用gpg加密文件: |
3)Base64是一组相似的编码方案,它将ASCII字符转换成以64为基数的形式,以可读的ASCII字符串来描述二进制数据。 base64命令可以用来编码/解码Base64字符串。要将文件编码为Base64格式,可以使用:
1 | $ base64 filename > outputfile |
4)md5sum与sha1sum都是单向散列算法,均无法逆推出原始数据。它们通常用于验证数据完整性或为特定数据生成唯一的密钥:
1 | $ md5sum file |
这种类型的散列算法是存储密码的理想方案。密码使用其对应的散列值来存储。如果某个用户需要进行认证,读取该用户提供的密码并转换成散列值,然后将其与之前存储的散列值进行比对。如果相同,用户就通过认证,被允许访问;否则,就会被拒绝访问。
5)openssl
用openssl生成shadow密码。 shadow密码通常都是salt密码。所谓SALT就是额外的一个字符串,用来起一个混淆的作用,使加密更加不易被破解。 salt由一些随机位组成,被用作密钥生成函数的输入之一,以生成密码的salt散列值。
1 | $ opensslpasswd -1 -salt SALT_STRING PASSWORD |
1 | # 对一组文件进行排序: |
检查文件是否已经排序过:
1 | #!/bin/bash |
-k
指定了排序应该按照哪一个键(key)来进行。键指的是列号,而列号就是执行排序时的依据。 -r
告诉sort命令按照逆序进行排序。例如:
1 | # 依据第1列,以逆序形式排序 |
有时文本中可能会包含一些像空格之类的不必要的多余字符。如果需要忽略这些字符,并以字典序进行排序,可以使用:
1 | $ sort -bd unsorted.txt |
sort选项:
1 | -b:忽略每行前面开始出的空格字符; |
uniq选项:
1 | -c或——count:在每列旁边显示该行重复出现的次数; |
wc选项:
1 | -c或--bytes或——chars:只显示Bytes数; # 统计字符数 |
1 | # 创建临时文件: |
如果提供了定制模板, X会被随机的字符(字母或数字)替换。注意, mktemp正常工作的前提是保证模板中只少要有3个X。
1 | # 将文件分割成多个大小为10KB的文件 |
上面的命令将data.file分割成多个文件,每一个文件大小为10KB。这些文件以xab、 xac、 xad的形式命名。这表明它们都有一个字母后缀。如果想以数字为后缀,可以另外使用-d参数。此外,使用 -a length可以指定后缀长度:
1 | $ split -b 10k data.file -d -a 4 |
除了k(KB)后缀,我们还可以使用M(MB)、 G(GB)、 c(byte)、 w(word)等后缀。
1 | # 为分割后的文件指定文件名前缀 |
csplit。它能够依据指定的条件和字符串匹配选项对日志文件进行分割。
1 | $ cat server.log |
有关这个命令的详细说明如下。
因为分割后的第一个文件没有任何内容(匹配的单词就位于文件的第一行中),所以我们删除了server00.log。
借助 %
操作符可以轻松将名称部分从 “名称.扩展名” 这种格式中提取出来。
1 | file_jpg="sample.jpg" |
将文件名的扩展名部分提取出来,这可以借助 # 操作符实现。
1 | extension=${file_jpg#*.} |
${VAR%.*}
的含义如下所述:
%属于非贪婪(non-greedy)操作。它从右到左找出匹配通配符的最短结果。还有另一个操作符 %%,这个操作符与%相似,但行为模式却是贪婪的,这意味着它会匹配符合条件的最长的字符串。
操作符%%则用.*从右向左执行贪婪匹配(.fun.book.txt)。
${VAR#*.}
的含义如下所述:
从$VAR中删除位于#右侧的通配符(即在前例中使用的*.)所匹配的字符串。通配
符从左向右进行匹配。
和 %% 类似, #也有一个相对应的贪婪操作符 ##。
##
从左向右进行贪婪匹配,并从指定变量中删除匹配结果。
这里有个能够提取域名不同部分的实用案例。假定 URL="www.google.com"
:
1 | $ echo ${URL%.*} # 移除.*所匹配的最右边的内容 |
1 | # 将 *.JPG更名为 *.jpg: |
1 | $ dd if=/dev/zero of=junk.data bs=1M count=1 |
该命令会创建一个1MB大小的文件junk.data。来看一下命令参数: if代表输入文件(input file),of代表输出文件(output file), bs代表以字节为单位的块大小(block size), count代表需要被复制的块数。
使用dd命令时一定得留意,该命令运行在设备底层。要是你不小心出了岔子,搞不好会把磁盘清空或是损坏数据。所以一定要反复检查dd命令所用的语法是否正确,尤其是参数of=。
单元大小 | 代码 |
---|---|
字节(1B) | c |
字(2B) | w |
块(512B) | b |
千字节(1024B) | k |
兆字节(1024KB) | M |
吉字节(1024MB) | G |
ls -lS
对当前目录下的所有文件按照文件大小进行排序,并列出文件的详细信息。
用命令ls -l可以列出文件的权限:
1 | -rw-r--r-- 1 slynux slynux 2497 2010-02-28 11:22 bot.py |
-
—— 普通文件。 1 | # 更改所有权 |
chattr能够将文件设置为不可修改。
1 | # 使用下列命令将一个文件设置为不可修改: |
1 | # 创建符号链接: |
1 | # 用下面的命令打印文件类型信息: |
1 | # 下面的命令可以创建一个1GB大小的文件: |
1 | #用下面的命令从/dev/cdrom创建一个ISO镜像: |
1 | - # 指定要显示多少行的文本。此参数必须与-c或-u参数一并使用。 |
生成目录的差异信息 :
1 | $ diff -Naur directory1 directory2 |
1 | # 生成patch文件 |
1 | $ more [参数选项] [文件] |
举例:
1 | # 显示提示,并从终端或控制台顶部显示; |
more 的动作指令:
1 | Enter # 向下n行,需要定义,默认为1行; |
其它命令通过管道和more结合的运用例子:
1 | $ ls -l /etc |more |
1 | -c # 从顶部(从上到下)刷新屏幕,并显示文件内容。而不是通过底部滚动完成刷新; |
less的动作命令:
1 | 回车键 # 向下移动一行; |
head 是显示一个文件的内容的前多少行:
1 | $ head -n 10 /etc/profile |
tail 是显示一个文件的内容的最后多少行:
1 | $ tail -n 5 /etc/profile |
1 | $ type getopt |
getopts不能直接处理长的选项(如:–prefix=/home等)
关于getopts的使用方法,可以man bash 搜索getopts。
getopts有两个参数,第一个参数是一个字符串,包括字符和“:”,每一个字符都是一个有效的选项,如果字符后面带有“:”,表示这个字符有自己的参数。getopts从命令中获取这些参数,并且删去了“-”,并将其赋值在第二个参数中,如果带有自己参数,这个参数赋值在 $OPTARG
中。提供getopts的shell内置了 $OPTARG
这个变变,getopts修改了这个变量。
这里变量 $OPTARG
存储相应选项的参数,而 $OPTIND
总是存储原始 $*
中下一个要处理的元素位置。while getopts ":a:bc" opt
#第一个冒号表示忽略错误;字符后面的冒号表示该选项必须有自己的参数
getopts后面的字符串就是可以使用的选项列表,每个字母代表一个选项,后面带:的意味着选项除了定义本身之外,还会带上一个参数作为选项的值,比如d:在实际的使用中就会对应-d 30,选项的值就是30;getopts字符串中没有跟随:的是开关型选项,不需要再指定值,相当于true/false,只要带了这个参数就是true。如果命令行中包含了没有在getopts列表中的选项,会有警告信息,如果在整个getopts字符串前面也加上个:,就能消除警告信息了。
两个特殊变量:
1 | $OPTIND # 特殊变量,option index,会逐个递增, 初始值为1 |
例子:
1 | echo $* |
1 | $ ./getopts.sh -a 11 -b -c |
具体用用法可以 man getopt
-o
表示短选项,两个冒号表示该选项有一个可选参数,可选参数必须紧贴选项,如 -carg
而不能是 -c arg
。
--long
表示长选项
例子:
1 | #!/bin/bash |
1 | $ ./getopt.sh --b-long abc -a -c33 remain |
1 | # 使用ls –d: |
使用pushd和popd时,可以无视cd命令。
1 | # 压入并切换路径: |
1 | # 重点标记出匹配某种样式的文件: |
正则表达式 | 描述 | 示例 |
---|---|---|
^ | 行起始标记 | ^tux 匹配以tux起始的行 |
$ | 行尾标记 | tux$ 匹配以tux结尾的行 |
. | 匹配任意一个字符 | Hack.匹配Hackl和Hacki,它只能匹配单个字符 |
[ ] | 匹配包含在 [字符] 之中的任意一个字符 | coo[kl] 匹配cook或cool |
[ ^ ] | 匹配除 [^字符] 之外的任意一个字符 | 9[^01] 匹配92、 93,但是不匹配91或90 |
[ - ] | 匹配 [ ] 中指定范围内的任意一个字符 | [1-5] 匹配从1~5的任意一个数字 |
? | 匹配之前的项1次或0次 | colou?r 匹配color或colour,但是不能匹配colouur |
+ | 匹配之前的项1次或多次 | Rollno-9+ 匹配Rollno-99、Rollno-9,但是不能匹配Rollno- |
* | 匹配之前的项0次或多次 | co*l 匹配cl、 col、 coool等 |
( ) | 创建一个用于匹配的子串 | ma(tri)?x 匹配max或maxtrix |
{n} | 匹配之前的项n次 | [0-9]{3} 匹 配 任 意 一 个 三 位 数 , [0-9]{3} 可 以 扩 展 为[0-9][0-9][0-9] |
{n, } | 之前的项至少需要匹配n次 | [0-9]{2,} 匹配任意一个两位或更多位的数字 |
{n, m} | 指定之前的项所必需匹配的最小次数和最大次数 | [0-9]{2,5} 匹配从两位数到五位数之间的任意一个数字 |
| | 交替——匹配 | 两边的任意一项 | Oct (1st | 2nd) 匹配Oct 1st或Oct 2nd |
\ | 转义符可以将上面介绍的特殊字符进行转义 | a\.b 匹配a.b,但不能匹配ajb。通过在 . 之间加上前缀 \ ,从而忽略了 . 的特殊意义 |
正则表达式 | 描述 |
---|---|
[:alnum:] | 所有的字母和数字 |
[:alpha:] | 所有字母 |
[:blank:] | 水平制表符,空白等 |
[:cntrl:] | 所有控制字符 |
[:digit:] | 所有的数字 |
[:graph:] | 所有可打印字符,不包括空格 |
[:lower:] | 所有的小写字符 |
[:print:] | 所有可打印字符,包括空格 |
[:punct:] | 所有的标点字符 |
[:space:] | 所有的横向或纵向的空白 |
[:upper:] | 所有大写字母 |
1 | -a# 不要忽略二进制的数据。 |
1 | # 单个grep命令也可以对多个文件进行搜索: |
1 | # 显示第2列和第3列: |
记法 | 范围 |
---|---|
N - | 从第N个字节,字符或字段到行尾 |
N - M | 从第N个字节,字符或字段到第M个(包括第M个在内)字节、字符或字段 |
- M | 第1个字节,字符或字段到第M个(包括第M个在内)字节、字符或字段 |
结合下列选项将字段指定为某个范围内的字节或字符 :
1 | $ cat range_fields.txt |
选项:
1 | -e <script># 以选项中指定的script来处理输入的文本文件 |
命令:
1 | a\ # 在当前行下面插入文本。 |
sed 替换标记:
1 | g # 表示行内全面替换。 |
sed 元字符集:
1 | ^ # 匹配行开始,如:/^sed/匹配所有以sed开头的行。 |
1 | # sed可以替换给定文本中的字符串。 |
-F fs
fs指定输入分隔符,fs可以是字符串或正则表达式,如-F:
-v var=value
赋值一个用户定义变量,将外部变量传递给awk -f scripfile
从脚本文件中读取awk命令 -m[fr] val
对val值设置内在限制,-mf
选项限制分配给val的最大块数目;-mr
选项限制记录的最大数目。这两个功能是Bell实验室版awk的扩展功能,在标准awk中不适用。1 | $ awk 'BEGIN{ print "start" } pattern{ commands } END{ print "end" }' file |
1 | $ awk 'BEGIN{ commands } pattern{ commands } END{ commands }' |
BEGIN{ commands }
语句块中的语句pattern{ commands }
语句块,它逐行扫描文件,从第一行到最后一行重复这个过程,直到文件全部被读取完毕END{ commands }
语句块说明: [A][N][P][G]
表示第一个支持变量的工具,[A]=awk
、[N]=nawk
、[P]=POSIXawk
、[G]=gawk
1 | $n # 当前记录的第n个字段,比如n为1表示第一个字段,n为2表示第二个字段。 |
1 | $ echo -e "line1 f2 f3nline2 f4 f5nline3 f6 f7" | awk '{print "Line No:"NR", No of fields:"NF, "$0="$0, "$1="$1, "$2="$2, "$3="$3}' |
借助 -v
选项,可以将外部值(并非来自stdin)传递给awk:
1 | $ VAR=10000 |
算数运算符:
运算符 | 描述 |
---|---|
+ - | 加、减 |
* / & | 乘,除与求余 |
+ - ! | 一元加、减和逻辑非 |
^ *** | 求幂 |
++ – | 增加或减少,作为前缀或后缀 |
1 | $ awk 'BEGIN{a="b";print a++,++a;}' |
**注意:**所有用作算术运算符进行操作,操作数自动转为数值,所有非数值都变为0
赋值运算符:
运算符 | 描述 |
---|---|
= += -= = /= %= ^= *= | 赋值语句 |
逻辑运算符:
运算符 | 描述 |
---|---|
|| | 逻辑或 |
&& | 逻辑与 |
1 | $ awk 'BEGIN{a=1;b=2;print (a>5 && b<=2),(a>5 || b<=2);}' |
正则运算符:
运算符 | 描述 |
---|---|
~ ~! | 匹配正则表达式和不匹配正则表达式 |
1 | $ awk 'BEGIN{a="100testa";if(a ~ /^100*/){print "ok";}}' |
关系运算符:
运算符 | 描述 |
---|---|
< <= > >= != == | 关系运算符 |
1 | $ awk 'BEGIN{a=11;if(a >= 9){print "ok";}}' |
**注意:**> < 可以作为字符串比较,也可以用作数值比较,关键看操作数如果是字符串就会转换为字符串比较。两个都为数字才转为数值比较。字符串比较:按照ASCII码顺序比较。
其他运算符:
运算符 | 描述 |
---|---|
$ | 字段引用 |
空格 | 字符串连接符 |
? : | C条件表达式 |
in | 数组中是否存在某键值 |
1 | $ awk 'BEGIN{a="b";print a=="b"?"ok":"err";}' |
运算级优先级表:
级别 | 运算符 | 说明 |
---|---|---|
1 | =, +=, -=, *=, /=, %=, &=, ^=, |=, <<=, >>= | 赋值、运算 |
2 | || | 逻辑或 |
3 | && | 逻辑与 |
4 | | | 按位或 |
5 | ^ | 按位异或 |
6 | & | 按位与 |
7 | ==, != | 等于、不等于 |
8 | <=, >=, <, > | 小于等于、大于等于、小于、大于 |
9 | <<, >> | 按位左移,按位右移 |
10 | +, - | 加、减 |
11 | *, /, % | 乘、除、取模 |
12 | !, ~ | 逻辑非、按位取反或补码 |
13 | -, + | 正、负 |
级别越高越优先
读取下一条记录:
awk中 next
语句使用:在循环逐行匹配,如果遇到 next
,就会跳过当前行,直接忽略下面语句。而进行下一行匹配。net语句一般用于多行合并:
1 | $ cat text.txt |
当记录行号除以2余1,就跳过当前行。下面的 print NR,$0
也不会执行。下一行开始,程序有开始判断 NR%2
值。这个时候记录行号是 :2
,就会执行下面语句块:'print NR,$0'
分析发现需要将包含有 “web” 行进行跳过,然后需要将内容与下面行合并为一行:
1 | $ cat text.txt |
简单地读取一条记录:
awk getline
用法:输出重定向需用到 getline函数
。getline从标准输入、管道或者当前正在处理的文件之外的其他输入文件获得输入。它负责从输入获得下一行的内容,并给NF,NR和FNR等内建变量赋值。
如果得到一条记录,getline函数返回1,如果到达文件的末尾就返回0,如果出现错误,例如打开文件失败,就返回-1。
getline语法:getline var,变量var包含了特定行的内容。
awk getline从整体上来说,用法说明:
无
重定向符|
或 <
时:getline作用于当前文件,读入当前文件的第一行给其后跟的变量 var
或 $0
(无变量),应该注意到,由于awk在处理getline之前已经读入了一行,所以getline得到的返回结果是隔行的。有
重定向符|
或 <
时:getline则作用于定向输入文件,由于该文件是刚打开,并没有被awk读入一行,只是getline读入,那么getline返回的是该文件的第一行,而不是隔行。1 | # 执行linux的date命令,并通过管道输出给getline,然后再把输出赋值给自定义变量out,并打印它: |
关闭文件:
awk中允许在程序中关闭一个输入或输出文件,方法是使用awk的close语句。
1 | close("filename") |
filename可以是getline打开的文件,也可以是stdin,包含文件名的变量或者getline使用的确切命令。或一个输出文件,可以是stdout,包含文件名的变量或使用管道的确切命令。
输出到一个文件:
1 | $ echo | awk '{printf("hello word!n") > "datafile"}' |
默认的字段定界符是空格
,可以使用 `-F "定界符"` 明确指定一个定界符:1 | $ awk -F: '{ print $NF }' /etc/passwd |
在 BEGIN语句块
中则可以用 OFS=“定界符”
设置输出字段的定界符。
条件判断语句:
1 | $ awk 'BEGIN{ |
每条命令语句后面可以用 ;
分号
结尾。循环语句:
while语句:
1 | $ awk 'BEGIN{ |
for循环:
格式1:
1 | $ awk 'BEGIN{ |
注:ENVIRON是awk常量,是子典型数组。
格式2:
1 | $ awk 'BEGIN{ |
do循环:
1 | $ awk 'BEGIN{ |
其他语句:
1 | # 得到数组长度 |
awk内置函数,主要分以下3种类似:算数函数、字符串函数、其它一般函数、时间函数。
算数函数:
格式 | 描述 |
---|---|
atan2( y, x ) | 返回 y/x 的反正切 |
cos( x ) | 返回 x 的余弦;x 是弧度 |
sin( x ) | 返回 x 的正弦;x 是弧度 |
exp( x ) | 返回 x 幂函数 |
log( x ) | 返回 x 的自然对数 |
sqrt( x ) | 返回 x 平方根 |
int( x ) | 返回 x 的截断至整数的值 |
rand( ) | 返回任意数字 n,其中 0 <= n < 1 |
srand( [expr] ) | 将 rand 函数的种子值设置为 Expr 参数的值,或如果省略 Expr 参数则使用某天的时间。返回先前的种子值。 |
1 | $ awk 'BEGIN{OFMT="%.3f";fs=sin(1);fe=exp(10);fl=log(10);fi=int(3.1415);print fs,fe,fl,fi;}' |
字符串函数:
格式 | 描述 |
---|---|
gsub( Ere, Repl, [ In ] ) | 除了正则表达式所有具体值被替代这点,它和 sub 函数完全一样地执行 |
sub( Ere, Repl, [ In ] ) | 用 Repl 参数指定的字符串替换 In 参数指定的字符串中的由 Ere 参数指定的扩展正则表达式的第一个具体值。sub 函数返回替换的数量。出现在 Repl 参数指定的字符串中的 &(和符号)由 In 参数指定的与 Ere 参数的指定的扩展正则表达式匹配的字符串替换。如果未指定 In 参数,缺省值是整个记录($0 记录变量) |
index( String1, String2 ) | 在由 String1 参数指定的字符串(其中有出现 String2 指定的参数)中,返回位置,从 1 开始编号。如果 String2 参数不在 String1 参数中出现,则返回 0(零) |
length [(String)] | 返回 String 参数指定的字符串的长度(字符形式)。如果未给出 String 参数,则返回整个记录的长度($0 记录变量) |
blength [(String)] | 返回 String 参数指定的字符串的长度(以字节为单位)。如果未给出 String 参数,则返回整个记录的长度($0 记录变量) |
substr( String, M, [ N ] ) | 返回具有 N 参数指定的字符数量子串。子串从 String 参数指定的字符串取得,其字符以 M 参数指定的位置开始。M 参数指定为将 String 参数中的第一个字符作为编号 1。如果未指定 N 参数,则子串的长度将是 M 参数指定的位置到 String 参数的末尾 的长度 |
match( String, Ere ) | 在 String 参数指定的字符串(Ere 参数指定的扩展正则表达式出现在其中)中返回位置(字符形式),从 1 开始编号,或如果 Ere 参数不出现,则返回 0(零)。RSTART 特殊变量设置为返回值。RLENGTH 特殊变量设置为匹配的字符串的长度,或如果未找到任何匹配,则设置为 -1(负一) |
split( String, A, [Ere] ) | 将 String 参数指定的参数分割为数组元素 A[1], A[2], . . ., A[n],并返回 n 变量的值。此分隔可以通过 Ere 参数指定的扩展正则表达式进行,或用当前字段分隔符(FS 特殊变量)来进行(如果没有给出 Ere 参数)。除非上下文指明特定的元素还应具有一个数字值,否则 A 数组中的元素用字符串值来创建 |
tolower( String ) | 返回 String 参数指定的字符串,字符串中每个大写字符将更改为小写。大写和小写的映射由当前语言环境的 LC_CTYPE 范畴定义 |
toupper( String ) | 返回 String 参数指定的字符串,字符串中每个小写字符将更改为大写。大写和小写的映射由当前语言环境的 LC_CTYPE 范畴定义 |
sprintf(Format, Expr, Expr, . . . ) | 根据 Format 参数指定的 printf 子例程格式字符串来格式化 Expr 参数指定的表达式并返回最后生成的字符串 |
注:Ere都可以是正则表达式。
1 | # gsub,sub使用 |
格式化字符串输出(sprintf使用)
格式化字符串格式:
格式 | 描述 |
---|---|
%d | 十进制有符号整数 |
%u | 十进制无符号整数 |
%f | 浮点数 |
%s | 字符串 |
%c | 单个字符 |
%p | 指针的值 |
%e | 指数形式的浮点数 |
%x | %X 无符号以十六进制表示的整数 |
%o | 无符号以八进制表示的整数 |
%g | 自动选择合适的表示法 |
1 | $ awk 'BEGIN{n1=124.113;n2=-1.224;n3=1.2345; printf("%.2f,%.2u,%.2g,%X,%on",n1,n2,n3,n1,n1);}' |
一般函数:
格式 | 描述 |
---|---|
close( Expression ) | 用同一个带字符串值的 Expression 参数来关闭由 print 或 printf 语句打开的或调用 getline 函数打开的文件或管道。如果文件或管道成功关闭,则返回 0;其它情况下返回非零值。如果打算写一个文件,并稍后在同一个程序中读取文件,则 close 语句是必需的 |
system(command ) | 执行 Command 参数指定的命令,并返回退出状态。等同于 system 子例程 |
Expression | getline [ Variable ] | 从来自 Expression 参数指定的命令的输出中通过管道传送的流中读取一个输入记录,并将该记录的值指定给 Variable 参数指定的变量。如果当前未打开将 Expression 参数的值作为其命令名称的流,则创建流。创建的流等同于调用 popen 子例程,此时 Command 参数取 Expression 参数的值且 Mode 参数设置为一个是 r 的值。只要流保留打开且 Expression 参数求得同一个字符串,则对 getline 函数的每次后续调用读取另一个记录。如果未指定 Variable 参数,则 $0 记录变量和 NF 特殊变量设置为从流读取的记录 |
getline [ Variable ] < Expression | 从 Expression 参数指定的文件读取输入的下一个记录,并将 Variable 参数指定的变量设置为该记录的值。只要流保留打开且 Expression 参数对同一个字符串求值,则对 getline 函数的每次后续调用读取另一个记录。如果未指定 Variable 参数,则 $0 记录变量和 NF 特殊变量设置为从流读取的记录 |
getline [ Variable ] | 将 Variable 参数指定的变量设置为从当前输入文件读取的下一个输入记录。如果未指定 Variable 参数,则 $0 记录变量设置为该记录的值,还将设置 NF、NR 和 FNR 特殊变量 |
1 | # 打开外部文件(close用法) |
时间函数:
格式 | 描述 |
---|---|
函数名 | 说明 |
mktime( YYYY MM dd HH MM ss[ DST]) | 生成时间格式 |
strftime([format [, timestamp]]) | 格式化时间输出,将时间戳转为时间字符串 具体格式,见下表. |
systime() | 得到时间戳,返回从1970年1月1日开始到当前时间(不计闰年)的整秒数 |
1 | # 建指定时间(mktime使用) |
strftime日期和时间格式说明符
格式 | 描述 |
---|---|
%a | 星期几的缩写(Sun) |
%A | 星期几的完整写法(Sunday) |
%b | 月名的缩写(Oct) |
%B | 月名的完整写法(October) |
%c | 本地日期和时间 |
%d | 十进制日期 |
%D | 日期 08/20/99 |
%e | 日期,如果只有一位会补上一个空格 |
%H | 用十进制表示24小时格式的时间 |
%I | 用十进制表示12小时格式的时间 |
%j | 从1月1日期一年中的第几天 |
%m | 十进制表示的月份 |
%M | 十进制表示的分钟 |
%p | 12小时表示法(AM/PM) |
%S | 十进制表示的秒 |
%U | 十进制表示的一年中的第几个星期(星期天作为一个星期的开始) |
%w | 十进制表示的星期几(星期天是0) |
%W | 十进制表示的一年中的第几个星期(星期一作为一个星期的开始) |
%x | 重新设置本地日期(08/20/99) |
%X | 重新设置本地时间(12 : 00 : 00) |
%y | 两位数字表示的年(99) |
%Y | 当前月份 |
%Z | 时区(PDT) |
%% | 百分号(%) |
1 | # 将所有.cpp文件中的Copyright替换成Copyleft: |
1 | -a<日志文件>:# 在指定的日志文件中记录资料的执行过程; |
1 | # 使用wget下载单个文件 |
测试下载链接:
当你打算进行定时下载,你应该在预定时间测试下载链接是否有效。我们可以增加–spider参数进行检查。
1 | $ wget --spider URL |
如果下载链接正确,将会显示:
1 | Spider mode enabled. Check if remote file exists. |
这保证了下载能在预定的时间进行,但当你给错了一个链接,将会显示如下错误:
1 | $ wget --spider url |
你可以在以下几种情况下使用–spider参数:
1 | # 增加重试次数 |
镜像网站:
1 | $ wget --mirror -p --convert-links -P ./LOCAL URL |
下载整个网站到本地。
下载指定格式文件:
1 | $ wget -r -A.pdf url |
可以在以下情况使用该功能:
FTP下载:
1 | $ wget ftp-url |
可以使用wget来完成ftp链接的下载。
使用wget匿名ftp下载:
1 | $ wget ftp-url |
使用wget用户名和密码认证的ftp下载:
1 | $ wget --ftp-user=USERNAME --ftp-password=PASSWORD url |
常见参数:
1 | -A/--user-agent <string> # 设置用户代理发送给服务器 |
1 | # 不显示进度信息使用--silent选项。 |
其他参数:
1 | -a/--append # 上传文件时,附加到目标文件 |
get请求:
1 | # 使用curl命令: |
1 | # 使用wget命令: |
post请求:
1 | # 使用curl命令(通过-d参数,把访问参数放在里面): |
1 | # 使用wget命令:(--post-data参数来实现) |
tar支持的参数包括: A
、 c
、 d
、 r
、 u
、 x
、 f
和 v
1 | # 用tar对文件进行归档: |
压缩tar归档文件:
归档文件通常被压缩成下列格式之一:
不同的tar选项可以用来指定不同的压缩格式:
1 | # 为了让tar支持根据扩展名自动进行压缩,使用 -a或 --auto-compress选项: |
1 | # 创建测试文件: |
对于归档命令:
在列出给定cpio归档文件所有内容的命令中:
当使用命令进行提取时, -d用来表示提取。 cpio在覆盖文件时不会发出提示。
1 | # 要使用gzip压缩文件,可以使用下面的命令: |
1 | # 压缩归档文件 |
1 | # 对归档文件采用ZIP格式进行压缩: |
1 | # 压缩单个文件: |
squashfs是一种具有超高压缩率的只读型文件系统,这种文件系统能够将2GB~3GB的数据压缩成一个700MB的文件。
1 | # 添加源目录和文件,创建一个squashfs文件: |
rsync可以对位于不同位置的文件和目录进行同步,它利用差异计算以及压缩技术来最小化数据传输量。
rsync也支持压缩、加密等多种特性。
1 | # 将源目录复制到目的端: |
1 | # 创建文件系统/分区备份。 |
1 | # 手动设置网络接口的IP地址: |
traceroute,它可以显示分组途径的所有网关的地址。 traceroute信息可以帮助我们搞明白分组到达目的地需要经过多少跳(hop)。中途的网关或路由器的数量给出了一个测量网络上两个节点之间距离的度量
(metric)。 traceroute的输出如下:
1 | $ traceroute google.com |
fping的选项如下:
1 | $ fping -a 192.160.1/24 -g |
1 | # SSH的压缩功能,选项-C启用这一功能: |
计算机联网的主要目的就是资源共享。在资源共享方面,使用最多的是文件共享。有多种方法可以用来在网络中传输文件。这则攻略就讨论了如何用常见的协议FTP、 SFTP、 RSYNC和SCP传输文件。
通过FTP传输文件可以使用lftp命令,通过SSH连接传输文件可以使用sftp, RSYNC使用SSH与rsync命令, scp通过SSH进行传输。
文件传输协议(File Transfer Protocol, FTP) :
1 | # 要连接FTP服务器传输文件,可以使用: |
你可以在提示符后输入命令,如下所示。
lftp username@ftphost:~> get filename
lftp username@ftphost:~> put filename
FTP自动传输 :
ftp是另一个可用于FTP文件传输的命令。相比较而言, lftp的用法更灵活。 lftp和ftp为用户启动一个交互式会话(通过显示消息来提示用户输入)。
SFTP(Secure FTP,安全FTP) :
1 | $ cd /home/slynux |
rsync命令 :
rsync广泛用于网络文件复制及系统备份。
SCP(Secure Copy Program,安全复制程序) :
1 | $ scp filename user@remotehost:/home/path |
用SCP进行递归复制 :
1 | $ scp -r /home/slynux user@remotehost:/home/backups |
我们需要用ifconfig分配IP地址和子网掩码才能连接上有线网络。对于无线网络来说,还需要其他工具(如iwconfig和iwlist)来配置更多的参数。
iwlist工具扫描并列出可用的无线网络。用下面的命令进行扫描:
1 | $ iwlist scan |
sshfs允许你将远程文件系统挂载到本地挂载点上。
1 | # 将位于远程主机上的文件系统挂载到本地挂载点上: |
列出系统中的开放端口以及运行在端口上的服务的详细信息,可以使用以下命令:
1 | $ lsof -i |
用netstat查看开放端口与服务 :
1 | # netstat -tnp列出开放端口与服务: |
最简单的方法就是使用netcat命令(或nc)。我们需要两个套接字:一个用来侦听,一个用来连接。
1 | # 设置侦听套接字: |
在网络上进行快速文件复制 :
1 | # 在接收端执行下列命令: |
1 | # 阻塞发送到特定IP地址的流量: |
df
是disk free的缩写, du
是disk usage的缩写。
1 | # 找出某个文件(或多个文件)占用的磁盘空间: |
du提供磁盘使用情况信息,而df提供磁盘可用空间信息。
1 | $ df -h |
1 | $ time COMMAND |
三种不同类型的时:
time命令 一些可以使用的参数:
参数 | 描述 |
---|---|
%C | 进行计时的命令名称以及命令行参数 |
%D | 进程非共享数据区域的大小,以KB为单位 |
%E | 进程使用的real时间(挂钟时间),显示格式为[小时:]分钟:秒 |
%x | 命令的退出状态 |
%k | 进程接收到的信号数量 |
%W | 进程被交换出主存的次数 |
%Z | 系统的页面大小。这是一个系统常量,但在不同的系统中,这个常量值也不同 |
%P | 进程所获得的CPU时间百分比。这个值等于user+system时间除以总运行时间。结果以百分比形式显示 |
%K | 进程的平均总(data+stack+text)内存使用量,以KB为单位 |
%w | 进程主动进行上下文切换的次数,例如等待I/O操作完成 |
%c | 进程被迫进行上下文切换的次数(由于时间片到期) |
1 | # 获取当前登录用户的相关信息: |
watch命令可以用来在终端中以固定的间隔监视命令输出。
1 | $ watch ls |
用一种被称为轮替(rotation)的技术来限制日志文件的体积,一旦它超过了限定的大小,就对其内容进行抽取(strip),同时将 日志文件中的旧条目存储到日志目录中的归档文件内。旧的日志文件就会得以保存以便随后参阅。
logrotate
的配置目录位于/etc/logrotate.d。
1 | $ cat /etc/logrotate.d/program |
配置文件中各个参数的含义:
参数 | 描述 |
---|---|
missingok | 如果日志文件丢失,则忽略;然后返回(不对日志文件进行轮替) |
notifempty | 仅当源日志文件非空时才对其进行轮替 |
size 30k | 限制实施轮替的日志文件的大小。可以用1M表示1MB |
compress | 允许用gzip压缩较旧的日志 |
weekly | 指定进行轮替的时间间隔。可以是weekly、 yearly或daily |
rotate 5 | 这是需要保留的旧日志文件的归档数量。在这里指定的是5,所以这些文件名将会是program.log.1.gz、 program.log.2.gz等直到program.log.5.gz |
create 0600 root root | 指定所要创建的归档文件的模式、用户以及用户组 |
每一个标准应用进程都可以利用syslog记录日志信息。
使用命令logger通过syslogd记录日志。
Linux中一些重要的日志文件 :
日志文件 | 描述 |
---|---|
/var/log/boot.log | 系统启动信息 |
/var/log/httpd | Apache Web服务器日志 |
/var/log/messages | 发布内核启动信息 |
/var/log/auth.log | 用户认证日志 |
/var/log/dmesg | 系统启动信息 |
/var/log/mail.log | 邮件服务器日志 |
/var/log/Xorg.0.log | X服务器日志 |
1 | # 向系统日志文件/var/log/message中写入日志信息: |
入侵者定义为:屡次试图登入系统达两分钟以上,并且期间的登录过程全部失败。凡是这类用户都应该被检测出来并生成包含以下细节信息的报告:
为了处理SSH登录失败的情况,还得知道用户认证会话日志会被记录在日志文件/var/log/auth.log中。脚本需要扫描这个日志文件来检测出失败的登录信息,执行各种检查来获取所需要的数据。我们可以用host命令找出IP地址所对应的主机。
1 | # 交互式监视, iotop的-o选项只显示出那些正在进行I/O活动的进程: |
使用fsck的各种选项对文件系统错误进行检查和修复。
1 | # 要检查分区或文件系统的错误,只需要将路径作为fsck的参数: |
1 | # 为了包含更多的信息,可以使用-f(表示full)来显示多列,如下所示: |
选项-o可以使用不同的参数:
参数 | 描述 |
---|---|
pcpu | CPU占用率 |
pid | 进程ID |
ppid | 父进程ID |
pmem | 内存使用率 |
comm | 可执行文件名 |
cmd | 简单命令 |
user | 启动进程的用户 |
nice | 优先级 |
time | 累计的CPU时间 |
etime | 进程启动后流逝的时间 |
tty | 所关联的TTY设备 |
euid | 有效用户ID |
stat | 进程状态 |
1 | # top, 默认会输出一个占用CPU最多的进程列表。输出结果每隔几秒就会更新。 |
1 | # which, which命令用来找出某个命令的位置。 |
信号是Linux中的一种进程间通信机制。 当进程接收到一个信号时,它会通过执行对应的信号处理程序(signal handler)来进行响应。
1 | # 列出所有可用的信号: |
常用到的信号量:
1 | # killall命令通过命令名终止进程: |
1 | # wall命令用来向当前所有登录用户的终端写入消息。 |
1 | # 打印当前系统的主机名: |
以Bash为例,它的进程ID是4295(pgrep bash),那么就会有一个对应的目录/proc/4295。进程对应的目录中包含了大量有关进程的信息。 /proc/PID中一些重要的文件如下所示。
environ:包含与进程相关的环境变量。使用cat /proc/4295/environ,可以显示所有传递给该进程的环境变量
cwd: 是一个到进程工作目录(working directory)的符号链接
exe:是一个到当前进程所对应的可执行文件的符号链接
$ readlink /proc/4295/exe
/bin/bash
fd:包含了进程所使用的文件描述符
crontab任务配置基本格式:
1 | * * * * * command |
cron表中的每一个条目都由6部分组成,并按照下列顺序排列:
星号(*)指定命令应该在每个时间段执行。
除了数字还有几个个特殊的符号就是 "*"
、"/"
和 "-"
、","
,*
代表所有的取值范围内的数字,"/"
代表每的意思, "*/5"
表示每5个单位,"-"
代表从某个数字到某个数字, ","
分开几个离散的数字。以下举几个例子说明问题:
1 | # 指定每小时的第5分钟执行一次ls命令 |
配置用户定时任务的语法:
1 | $ crontab [-u user]file |
参数与说明:
1 | # 取整个屏幕: |
其中command-line是在终端上键入的一条普通命令行。然而当在它前面放上eval时,其结果是shell在执行命令行之前扫描它两次。如:
1 | $ pipe="|" |
shell第1次扫描命令行时,它替换出pipe的值|,接着eval使它再次扫描命令行,这时shell把|作为管道符号了。
如果变量中包含任何需要shell直接在命令行中看到的字符(不是替换的结果),就可以使用eval。命令行结束符(;| &),I/o重定向符(< >)和引号就属于对shell具有特殊意义的符号,必须直接出现在命令行中。
如:
1 | $ cat last |
第一遍扫描后,shell把反斜杠去掉了。当shell再次扫描该行时,它替换了$4的值,并执行echo命令
1 | $ x=100 |
HTTP mindmap整理
source from 《HTTP权威指南》
1.安装node
进入node官网进行下载。
版本查看:
1 | $ node -v |
**注意:**node版本最好新一点,推介6.0以上。
2.全局安装vue-cli
1 | $ npm install -g vue-cli |
注意: 如果安装失败可能需要root权限重新安装。
3.创建一个基于 webpack
模板的新项目
1 | $ vue init webpack project-name #(默认安装2.0版本) |
main.js是入口文件,主要作用是初始化vue实例并使用需要的插件
1 | // The Vue build version to load with the `import` command |
App.vue是我们的主组件,所有页面都是在App.vue下进行切换的。其实你也可以理解为所有的路由也是App.vue的子组件。所以我将router标示为App.vue的子组件。
1 | <template> |
index.html文件入口
src放置组件和入口文件
node_modules为依赖的模块
config中配置了路径端口值等
build中配置了webpack的基本配置、开发环境配置、生产环境配置等
1 | $ cd project-name |
认识HTTP—-Cookie和Session篇
彻底理解cookie,session,token
详解cookie、session和HTTP缓存
COOKIE和SESSION机制详解
TODO
]]>文章主要以一次HTTP请求的整个过程来讲解(DNS解析不讲):HTTP 起源、TCP/IP 协议、建立 TCP 连接、客户端请求、服务端响应、断开 TCP 连接,文章最后还捎带讲了与 HTTP 相关知识。
今天我们能够在网络中畅游,都得益于一位计算机科学家蒂姆·伯纳斯·李的构想。1991年8月6日,蒂姆·伯纳斯·李在位于欧洲粒子物理研究所(CERN)的NeXT计算机上,正式公开运行世界上第一个Web网站(http://info.cern.ch ),建立起基本的互联网基础概念和技术体系,由此开启了网络信息时代的序幕。
伯纳斯·李的提案包含了网络的基本概念并逐步建立了所有必要的工具:
提出HTTP (Hypertext Transfer Protocol) 超文本传输协议,允许用户通过单击超链接访问资源;
提出使用HTML超文本标记语言(Hypertext Markup Language)作为创建网页的标准;
创建了统一资源定位器URL (Uniform Resource Locator)作为网站地址系统,就是沿用至今的http://www URL格式;
创建第一个Web浏览器,称为万维网浏览器,这也是一个Web编辑器;
创建第一个Web服务器(http://info.cern.ch)以及描述项目本身的第一个Web页面。
HTTP 协议一共有五大特点:
我们经常听到一句话就是:HTTP是一个基于TCP/IP协议簇来传递数据。
如何理解上面那句话?我们来看看TCP/IP四层模型就明白了。
从上图我们可以清晰的看到HTTP使用的传输层协议为TCP协议,而网络层使用的是IP协议(当然还使用了很多其他协议),所以说HTTP是一个基于TCP/IP协议簇来传递数据。
同样我们可以看到 ping 走的 ICMP 协议,这也就是为什么有时候我们开 vps 可以上网,但是 ping google 却ping 不通的原因,因为走的是不同的协议。
那 TCP/IP 协议簇大致是如何工作的,我们再来看看下图:
我们可以看到在数据发送端是一层一层封装数据,数据接收端一层一层拆封,最后应用层获得数据。
我们知道了TCP/IP协议簇大致的工作原理之后,我们来看看HTTP是如何建立连接的。
前面咱们讲过HTTP是一个基于TCP/IP协议簇来传递数据,所以这HTTP建立连接也就是建立TCP连接,TCP如何建立连接,一起来看看TCP包信息结构吧。
TCP 报文包 = TCP 头信息 + TCP 数据体,而在 TCP 头信息中包含了 6 种控制位(上图红色框中),这六种标志位就代表着 TCP 连接的状态:
URG:紧急数据(urgent data)—这是一条紧急信息
ACK:确认已收到
PSH:提示接收端应用程序应该立即从tcp接受缓冲区中读走数据
RST:表示要求对方重新建立连接
SYN:表示请求建立一个连接
FIN:表示通知对方本端要关闭连接了
了解了TCP包头信息之后,我们就可以正式看看TCP建立连接的三次握手了。
三次握手讲解:
syn=1
,随机产生 seq number=1234567
的数据包到服务器,服务器由 SYN=1
知道客户端要求建立联机(客户端:我要连接你)ack number=(客户端的seq+1)
, syn=1
, ack=1
, 随机产生seq=7654321
的包(服务器:好的,你来连吧)ack number
是否正确,即第一次发送的 seq number+1
,以及位码 ack
是否为 1,若正确,客户端会再发送 ack number=(服务器的seq+1)
, ack=1
,服务器收到后确认 seq
值与 ack=1
则连接建立成功。(客户端:好的,我来了)面试官:为什么 http 建立连接需要三次握手,不是两次或四次
答:三次是最少的安全次数,两次不安全,四次浪费资源
客户端与服务器连接上了之后,客户端就可以开始向服务器请求资源,就可以开始发送HTTP请求了。
我们之前说过 TCP 报文包 = TCP 头信息 + TCP 数据体,TCP 头信息我们已经讲了,现在来讲 TCP 数据体,也就是我们的 HTTP 请求报文。
来看看实际的HTTP请求例子:
① 是请求方法,HTTP/1.1 定义的请求方法有 8 种:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE, 最常的两种 GET 和 POST,如果是 RESTful 接口的话一般会用到 GET、POST、DELETE、PUT
② 为请求对应的URL地址,它和报文头的 Host 属性组成完整的请求 URL
③ 是协议名称及版本号
④ 是HTTP的报文头,报文头包含若干个属性,格式为 “属性名 : 属性值”,服务端据此获取客户端的信息
⑤ 是报文体,它将一个页面表单中的组件值通过 param1=value1¶m2=value2
的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求 URL 也可以通过类似于 “/chapter15/user.html? param1=value1¶m2=value2
” 的方式传递请求参数。
请求头参数非常多,就不一一说明,只说明两个低级的反扒参数:
服务器在收到客户端请求处理完需要响应并返回给客户端,而HTTP响应报文结构与请求结构体一致。
响应报文中我们重点关注下:服务器的响应状态码,面试也很容易问到,下面只列出分类,详细状态码自行上网查找了解。
在服务器响应完毕后,一次会话就结束了,请问这时候连接会断开吗?
是否断开我们需要区分 HTTP 版本:
注意:长连接是指一次 TCP 连接允许多次 HTTP 会话,HTTP 永远都是一次请求/响应,会话结束,HTTP 本身不存在长连接之说。
早在 1999 年 HTTP1.1 就推广普及,所以现在浏览器在请求时请求头中都会携带一个参数:Connection:keep-alive
,这表示浏览器要求与服务器建立长连接,而服务器也可以设置是否愿意建立长连接。
对于服务器来说建立长连接有优点也有缺点:
所以是否开启长连接,长连接时间都需要根据网站自身来合理设置。
ps:大家不要小看这一个 TCP 连接,在一次客户端 HTTP 完整的请求中(DNS寻址、建立TCP连接、请求、等待、解析网页、断开TCP连接)建立 TCP 连接占用的时间比还是很大的。
在建立TCP连接时是三次握手,而断开TCP连接是四次挥手!
在前面讲 TCP/IP 协议时我们说过标志位:FIN 表示通知对方本端要关闭连接了,那断开连接为何需要四次挥手呢?
TODO
面试官:为何建立连接需要三次握手而关闭连接却需要四次挥手。
TODO
HTTP/1.1 已经为我们服务了20年,而 HTTP/2.0 其实在 2015 就发布了,但是还没有推广开来,关于 HTTP/2.0 新特性也可以去网上查阅相关资料.
因为 http 响应慢、请求头体积大等缺点,所以在微服务时代,大家都使用 rpc 来调用服务,rpc 相关概念感兴趣同学自行网上学习。
http还有两个很大的缺点就是明文且不能保证完整性,所以目前会渐渐被HTTPS代替。
]]>lighttpd
提供了一种外部程序调用的接口,即 FastCGI
接口。这是一种独立于平台和服务器的接口,它介于Web应用程序和Web服务器之间。
这就意味着能够在 Apache
服务器上运行的 FastCGI
程序,也一定可以无缝的在 lighttpd
上使用。
1)就像 CGI
一样,FastCGI
也是独立于编程语言的。
2)就像 CGI
一样,FastCGI
程序运行在完全独立于核心 Web Server
之外的进程中,和 API
方式相比,提供了很大的安全性。(API会将程序代码与核心Web Server挂接在一起,这就意味着基于问题API的应用程序可能会使整个Web Server或另一个应用程序崩溃;一个恶意API还可以从核心Web Server或另一个应用程序中盗取安全密钥)
3) 虽然 FastCGI
不能一夜之间复制CGI的所有功能,但是 FastCGI
一直宣扬开放,这也使得我们拥有很多免费的 FastCGI
应用程序库(C/C++、Java、Perl、TCL)和免费的Server模块(Apache、ISS、Lighttpd)。
4) 就像 CGI
一样,FastCGI
并不依附于任何 Web Server
的内部架构,因此即使 Server
的技术实现变动,FastCGI
仍然非常稳定;而 API
设计是反映 Web Server
内部架构的,因此,一旦架构改变,API要随之变动。
5) FastCGI
程序可以运行在任何机器上,完全可以和 Web Server
不在一台机器上。这种分布式计算的思想可以确保可扩展性、提高系统可用性和安全性。
6) CGI
程序主要是对 HTTP
请求做计算处理,而 FastCGI
却还可以做得更多,例如模块化认证、授权检查、数据类型转换等等。在未来,FastCGI
还会有能力扮演更多角色。
7) FastCGI
移除了 CGI
程序的许多弊端。例如,针对每一个新请求,WebServer
都必须重启 CGI
程序来处理新请求,这导致 WebServer
的性能会大受影响。而 FastCGI
通过保持进程处理运行状态并持续处理请求的方式解决了该问题,这就将进程创建和销毁的时间节省了出来。
8) CGI
程序需要通过管道(pipe)方式与 Web Server
通信,而 FastCGI
则是通过 Unix-Domain-Sockets
或 TCP/IP
方式来实现与 Web Server
的通信。这确保了 FastCGI
可以运行在 Web Server
之外的服务器上。FastCGI
提供了 FastCGI
负载均衡器,它可以有效控制多个独立的 FastCGI Server
的负载,这种方式比 load-balancer+apache+mod_php
方式能够承担更多的流量。
若要 lighttpd
支持 fastcgi
,则需要配置如下内容:
在 fastcgi.conf
中配置
1 | server.modules += ( "mod_fastcgi" ) |
及在 module.conf
中配置
1 | include "conf.d/fastcgi.conf" |
lighttpd
通过 fastcgi
模块的方式实现了对 fastcgi
的支持,并且在配置文件中提供了三个相关的选项:
1) fastcgi.debug
可以设置一个从0到65535的值,用于设定 FastCGI
模块的调试等级。当前仅有0和1可用。1表示开启调试(会输出调试信息),0表示禁用。例如:
1 | fastcgi.debug = 1 |
2) fastcgi.map-extentsions
同一个 fastcgi server
能够映射多个扩展名,如 .php3
和 .php4
都对应 .php
。例如:
1 | fastcgi.map-extensions = ( ".php3" => ".php" ) |
or for multiple
1 | fastcgi.map-extensions = ( ".php3" => ".php", ".php4" => ".php" ) |
3) fastcgi.server
这个配置是告诉 Web Server
将 FastCGI
请求发送到哪里,其中每一个文件扩展名可以处理一个类型的请求。负载均衡器可以实现对同一扩展名的多个对象的负载均衡。
fastcgi.server
的结构语法如下:
1 | ( <extension> => |
其中:
extentsion :文件名后缀或以”/”开头的前缀(也可为文件名)
name :这是一个可选项,表示handler的名称,在mod_status中用于统计功能,可以清晰的分辨出是哪一个handler处理了。host :FastCGI进程监听的IP地址。此处不支持hostname形式。
port :FastCGI进程所监听的TCP端口号
bin-path :本地FastCGI二进制程序的路径,当本地没有FastCGI正在运行时,会启动这个FastCGI程序。
socket :unix-domain-socket所在路径。
mode :可以选择FastCGI协议的模式,默认是“responder”,还可以选择authorizer。
docroot :这是一个可选项,对于responder模式来讲,表示远程主机docroot;对于authorizer模式来说,它表示MANDATORY,并且指向授权请求的docroot。
check_local :这是一个可选项,默认是enable。如果是enable,那么server会首先在本地(server.document-root)目录中检查被请求的文件是否存在,如果不存在,则给用户返回404(Not Found),而不会把这个请求传递给FastCGI。如果是disable,那么server不会检查本地文件,而是直接将请求转发给FastCGI。(disable的话,server从某种意义上说就变为了一个转发器)
broken-scriptfilename :以类似PHP抽取PATH_INFO的方式,抽取URL中的SCRIPT_FILENAME。
如果 bin-path
被设置了,那么:
max-procs :设置多少个FastCGI进程被启动
bin-environment :在FastCGI进程启动时设置一个环境变量
bin-copy-environment :清除环境,并拷贝指定的变量到全新的环境中。
kill-signal :默认的话,在停止FastCGI进程时,lighttpd会发送SIGTERM(-15)信号给子进程。此处可以设置发送的信号。
举例 :
使用前缀来对应主机:
1 | fastcgi.server = ( |
如果有一个请求 “http://my.example.org/remote_scripts/test.cgi",那么server会将其转发给192.168.0.3的9000端口,并且 SCRIPT_NAME
会被赋值为 “/remote_scripts/test.cgi”
。如果所设置的 handler
的末尾不是 “/”
,那么会被认为是一个文件。
负载均衡 :
FastCGI
模块提供了一种在多台 FastCGI
服务器间负载均衡的方法。
例如:
1 | fastcgi.server = ( ".php" => |
为了更好的理解负载均衡实现的原理,建议你置 fastcgi.debug
为 1
。即使对于本机的多个 FastCGI
,你也会获得如下输出:
1 | proc: 127.0.0.1 1031 1 1 1 31454 |
上述信息显示出了IP地址,端口号、当前链接数(也就是负载)(倒数第二列)、进程ID(倒数第一列)等等。整个输出信息总是以负载域来从小到大排序的。
说说lighttpd的fastcgi
Nginx + CGI/FastCGI + C/Cpp
FastCGI+lighttpd开发之介绍和环境搭建
1 | $ cat /etc/qtilighttpd.conf |
版本信息中含有(ssl)字样的信息说明支持ssl,可以在终端输入如下查看:
1 | $ lighttpd -v |
完整的ssl证书分为四个部分:
证书相当于公钥,pem相当于私钥。
Self-Signed Certificates:包含公钥和私钥的结合体,证书(公钥)会在连接请求的时候发给浏览器,以便浏览器解密和加密。
创建Self-Signed Certificates:
1 | $ openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes |
上边的命令生成一个server.pem文件。
1 | $SERVER["socket"] == "[::]:443" { |
下面是 lighttpd.conf
文件中关于强制 HTTP 定向到 HTTPS 的部分配置:
1 | $HTTP["scheme"] == "http" { |
此功能需要lighttpd mod_redirect
模块支持。使用此功能前确保模块已经安装。
禁用 SSL Compression (抵御 CRIME 攻击)
1 | ssl.use-compression = "disable" |
禁用 SSLv2 及 SSLv3
1 | ssl.use-sslv2 = "disable" |
抵御 Poodle 和 SSL downgrade 攻击
需要支持 TLS-FALLBACK-SCSV
以自动开启此功能。下列 openSSL 版本包含对 TLS-FALLBACK-SCSV
的支持,lighttpd 会自动启用此特性。
1.0.1j
及之后的版本中支持1.0.0o
及之后的版本中支持0.9.8zc
及之后的版本中支持加密及交换算法
一份推介的配置:
1 | ssl.cipher-list = "EECDH+AESGCM:EDH+AESGCM:AES128+EECDH:AES128+EDH" |
如果您需要兼容一些老式系统和浏览器 (例如 Windows XP 和 IE6),请使用下面的:
1 | ssl.cipher-list = "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" |
配置 Forward Secrecy 和 DHE 参数
生成强 DHE 参数:
1 | $ cd /etc/ssl/certs |
建议您使用性能强劲的平台生成此文件,例如最新版的至强物理机。如果您只有一台小型 VPS,请使用 openssl dhparam -out dhparam.pem 2048
命令生成 2048bit 的参数文件。
添加到 SSL 配置文件:
1 | ssl.dh-file = "/etc/ssl/certs/dhparam.pem" |
启用 HSTS
1 | server.modules += ( "mod_setenv" ) |
Lighttpd
]]>1 | $ svn checkout http://路径(目录或文件的全路径) [本地目录全路径] --username 用户名 |
注 :如果不带–password 参数传输密码的话,会提示输入密码,建议不要用明文的–password 选项。 不指定本地目录全路径,则检出到当前目录下。
1 | $ svn export [-r 版本号] http://路径(目录或文件的全路径) [本地目录全路径] --username 用户名 |
注 :第一种从版本库导出干净工作目录树的形式是指定URL,
如果指定了修订版本号,会导出相应的版本,
如果没有指定修订版本,则会导出最新的,导出到指定位置。
如果省略 本地目录全路径,URL的最后一部分会作为本地目录的名字。
第二种形式是指定 本地检出的目录全路径 到 要导出的本地目录全路径,所有的本地修改将会保留,
但是不在版本控制下(即没提交的新文件,因为.svn文件夹里没有与之相关的信息记录)的文件不会拷贝。
1 | $ svn add 文件名 |
1 | $ svn commit -m "提交备注信息文本" [-N] [--no-unlock] 文件名 |
1 | $ svn update |
1 | $ svn delete svn://路径(目录或文件的全路径) -m "删除备注信息文本" |
1 | $ svn lock -m "加锁备注信息文本" [--force] 文件名 |
1 | $ svn diff 文件名 |
1 | $ svn st 目录路径/名 |
注 :svn status、svn diff和 svn revert这三条命令在没有网络的情况下也可以执行的,原因是svn在本地的.svn中保留了本地版本的原始拷贝。
1 | $ svn log 文件名 |
1 | $ svn info 文件名 |
1 | # 全部功能选项 |
1 | $ svn list svn://路径(目录或文件的全路径) |
1 | $ svn mkdir 目录名 |
注 : 添加完子目录后,一定要回到根目录更新一下,不然在该目录下提交文件会提示“提交失败”
1 | $ svn update |
注 :如果手工在checkout出来的目录里创建了一个新文件夹newsubdir,
再用svn mkdir newsubdir命令后,SVN会提示:
svn: 尝试用 “svn add”或 “svn add –non-recursive”代替?
svn: 无法创建目录“hello”: 文件已经存在
此时,用如下命令解决:svn add --non-recursive newsubdir
在进入这个newsubdir文件夹,用ls -a查看它下面的全部目录与文件,会发现多了:.svn目录
再用 svn mkdir -m “添hello功能模块文件” svn://localhost/test/newdir/newsubdir 命令,
SVN提示:
svn: File already exists: filesystem ‘/data/svnroot/test/db’, transaction ‘4541-1’,
path ‘/newdir/newsubdir ‘
1 | $ svn revert [--recursive] 文件名 |
1 | $ svn switch http://目录全路径 本地目录全路径 |
1 | $ svn resolved [本地目录全路径] |
1 | $ svn cat http://文件全路径 |
首先确定已经安装好了 nodejs
和 npm
以及 git
1 | $ npm install hexo -g |
访问http://localhost:4000,会看到生成好的博客。
1 | |-- _config.yml |
_config.yml
全局配置文件,网站的很多信息都在这里配置,诸如网站名称,副标题,描述,作者,语言,主题,部署等等参数。这个文件下面会做较为详细的介绍。
package.json
hexo框架的参数和所依赖插件,如下:
1 | { |
scaffold
scaffolds是“脚手架、骨架”的意思,当你新建一篇文章(hexo new ‘title’)的时候,hexo是根据这个目录下的文件进行构建的。基本不用关心。
_config.yml文件
_config.yml 采用YAML语法格式,具体语法自行学习 。
具体配置可以参考官方文档,_config.yml 文件中的内容,并对主要参数做简单的介绍
1 | # Hexo Configuration |
如果页面中出现中文,应以UTF-8无BOM编码格式,所以不要用win自带的记事本,而是用notepad++这种支持编码转换的编辑器。
由于google在天朝大陆被墙,进入 themes\landscape\layout\_partial
,打开 head.ejs
,删掉第31行 fonts.googleapis.com
的链接。
下载下来 jQuery-2.0.3.min.js
,放到 themes\landscape\source\js
文件夹中。之后进入 themes\landscape\layout\_partial
,打开 after-footer.ejs
,将第17行的路径替换为 /js/jquery-2.0.3.min.js
。
至此大功告成。
命令行输入:
1 | $ hexo new post "new article" |
之后在 soource/_posts
目录下面多了一个 new-article.md
的文件。
Setting | Description | Default |
---|---|---|
layout | Layout | post或page |
title | 文章的标题 | |
date | 穿件日期 | 文件的创建日期 |
updated | 修改日期 | 文件的修改日期 |
comments | 是否开启评论 | true |
tags | 标签 | |
categories | 分类 | |
permalink | url中的名字 | 文件名 |
toc | 是否开启目录 | true |
reward | 是否开启打赏 | true |
1 | categories: |
<!--more-->
之上的内容为摘要。
草稿相当于很多博客都有的“私密文章”功能。
1 | $ hexo new draft "new draft" |
会在 source/_drafts
目录下生成一个 new-draft.md
文件。但是这个文件不被显示在页面上,链接也访问不到。也就是说如果你想把某一篇文章移除显示,又不舍得删除,可以把它移动到 _drafts
目录之中。
如果你希望强行预览草稿,更改配置文件:
1 | render_drafts: true |
或者,如下方式启动server:
1 | $ hexo server --drafts |
下面这条命令可以把草稿变成文章,或者页面:
1 | $ hexo publish [layout] <filename> |
文章推介:Hexo 博客中插入音乐/视频
使用七牛为Hexo存储图片
[hexo主题中添加相册功能](http://www.cnblogs.com/xljzlw/p/5137622.html)
为 Hexo 主题添加多种图片样式(主题不错考虑移植)
Hexo折腾记——基本配置篇
hexo博客进阶-相册和独立域名
插入图片基本分为两种办法** :
(1) 放在本地文件
首先在根目录下确认 _config.yml
中有 post_asset_folder:true
。
在 hexo 目录,执行:
1 | $ npm install https://github.com/CodeFalling/hexo-asset-image --save |
之后再使用 hexo new 'new'
创建新博客的时候,会在 source/_posts
里面创建 .md
文件的同时生成一个相同的名字的文件夹。把该文章中需要使用的图片放在该文件夹下即可。
使用的时候
1 | ![“图片描述”(可以不写)](/文件夹名/你的图片名字.JPG) |
(2)放在七牛上,需要先注册,上传图片生成链接,直接在文章中使用链接即可。
插入音乐 :
可以使用网易云音乐,搜索想要的歌曲,点击歌曲名字进入播放器页面,点击生成外链播放器;复制代码,直接粘贴到博文中即可。这样会显示一个网易的播放器,可以把
1 | <iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=298 height=52 src="http://music.163.com/outchain/player?type=2&id=32192436&auto=1&height=32"></iframe> |
highlightjs官网
highlightjs主题风格
Hexo,Yilia主题添加站内搜索功能
为Hexo博客添加目录
Hexo站点中添加文章目录以及归档
使用LeanCloud平台为Hexo博客添加文章浏览量统计组件
使用hexo搭建静态博客
Hexo Docs中文 : (二)基本用法
]]>官方手册
中文官网
vuejs 2.0 中文文档
ECMAScript 6 入门
node.js相关的中文文档及教程
Node.js中文网API
Webpack 中文指南
webpack2.2中文文档
以上是提供的一些官方资料,下面开始我们的套路吧:
1.新建一个目录vuepro
2.初始化
1 | $ cd vuepro |
3.安装模块,先装这么多,有需要再安装
1 | $ npm install vue webpack babel-loader babel-core babel-preset-env babel-cli babel-preset-es2015 html-webpack-plugin --save-dev |
4.创建良好的目录层级
1 | $ mkdir src |
html
放置模板文件,jssrc
放置js文件,最终编译好的文件放置在webapp
目录里,这个目录也就是我们网站的目录。
5.在项目根目录下创建webpack配置文件:webpack.config.js
1 | var HtmlWebpackPlugin = require('html-webpack-plugin'); |
6.同样在根目录下创建babel配置文件:.babelrc
1 | { |
然后就可以在webpack里面配置loader,我们上面webpack配置中已经写了:
1 | loaders:[ |
这句话意思就是:凡是 .js
文件都使用 babel-loader
, 并且压缩。
思考:数据如何渲染?
套路如下:
首先要有个数据块标记
vue里面可以像模板引擎一样写上 {\{name\}}
其中 name
就是变量名
index.htm l如下:
1 |
|
index.js 如下:
1 | import Vue from "vue"; //会去node_modules\vue\package.json |
至此,我们需要用 webpack
打包,打包到 webapp
目录下。
需要修改2个地方:
(1)因为我们的 webpack
不是全局安装的,所以不能直接执行 webpack
命令,我们这里借助 npm
来执行。所以需要修改项目根目录下的 package.json
文件,加入:
1 | "scripts": { |
表示:执行build,就会去node_modules.bin\下去寻找webpack命令。build
这个名字是自定义的。
(2)还需要修改 webpack 配置文件:webpack.config.js
1 | resolve: { |
我们之前把这个注释掉了,现在打开。此处的意义是找到 node_modules/vue/dist/vue.js
最后,我们就来打包,看看结果是怎样的?
终端里还是cd到项目根目录下,执行:
1 | $ npm run build |
index.html
就是打包之后的模板文件,js/index.js
就是打包之后的js文件,在 index.html
被引用了。
1 |
|
预览一下index.html:
这样就完成了 vueJS
的一个简单案列
主体的思路是将博文内容相关文件放在Github项目中master中,将Hexo配置写博客用的相关文件放在Github项目的hexo分支上,这个是关键,多终端的同步只需要对分支hexo进行操作。下面是详细的步骤讲解:
安装了Node.js,Git,Hexo环境
完成Github与本地Hexo的对接
这部分大家可以参考史上最详细的Hexo博客搭建图文教程
配置好这些,就可以捋起袖子大干一场了!
在利用Github+Hexo搭建自己的博客时,新建了一个Hexo的文件夹,并进行相关的配置,这部分主要是将这些配置的文件托管到Github项目的分支上,其中只托管部分用于多终端的同步的文件,如完成的效果图所示:
1 | # 初始化本地仓库 |
这样你的github项目中就会多出一个Hexo分支,这个就是用于多终端同步关键的部分。
此时在另一终端更新博客,只需要将Github的hexo分支clone下来,进行初次的相关配置
1 | # 将Github中hexo分支clone到本地 |
在不同的终端已经做完配置,就可以愉快的分享自己更新的博客
进入自己相应的文件夹
1 | # 先pull完成本地与远端的融合 |
【Git】子模块:一个仓库包含另一个仓库
如何在大型项目中使用Git子模块开发
git子模块
到目前为止,将您的大项目分解为子项目.
现在使用以下命令将每个子项目添加到主项目:
1 | git submodule add <url> |
项目添加到您的仓库后,您必须初始化并更新它.
1 | git submodule init |
从Git 1.8.2开始,新选项 –remote
被添加
1 | git submodule update --remote --merge |
将从每个子模块的上游获取最新的更改,将它们合并,并检查子模块的最新版本.
基于公司的项目会越来越多,常常需要提取一个公共的类库提供给多个项目使用,但是这个library
怎么和git
在一起方便管理呢?
我们需要解决下面几个问题:
library库
?library库
在其他的项目中被修改了可以更新到远程的代码库中?library库
最新的提交?library库
?解决以上问题,可以考虑使用git的 Submodule
来解决。
git Submodule
是一个很好的多项目使用共同类库的工具,他允许类库项目做为repository
,子项目做为一个单独的git项目
存在父项目中,子项目可以有自己的独立的commit
,push
,pull
。而父项目以Submodule
的形式包含子项目,父项目可以指定子项目header
,父项目中会的提交信息包含Submodule
的信息,再clone父项目
的时候可以把Submodule
初始化。
使用git
命令可以直接添加Submodule
:
1 | git submodule add git@github.com:xxx.git pod-library |
使用 git status
命令可以看到
1 | git status |
1 | On branch master |
可以看到多了两个需要提交的文件:.gitmodules
和 pod-library
.gitmodules
内容包含Submodule
的主要信息,指定reposirory
,指定路径:
1 | [submodule "pod-library"] |
可以看到记录了子项目的目录和子项目的git
地址信息。
pod-libray
内容只保护子项目的commit id
,就能指定到对于的git header
上,例如:
1 | Subproject commit 4ac42d2f8b9ba0c2f0f2f2ec87ddbd529275fea5 |
4ac42d2f8b9ba0c2f0f2f2ec87ddbd529275fea5
就是子项目的commit id
,父项目的git并不会记录Submodule
的文件变动,它是按照commit git
指定Submodule
的git header
。
另外,这两个文件都需要提交到父项目的git中。
还可以这样使用命令添加Submodule
1 | git add .gitmodules pod-ibrary |
首先需要确认有对Submodule的commit权限。
进入Submodule
目录里面:
1 | cd pod-library/ |
修改其中的一个文件看下文件的可以用git status
查看变动:
1 | git status |
提交Submodule
的更改内容:
1 | git commit -a -m'test submodule' |
然后push
到远程服务器:
1 | git push |
然后再回到父目录,提交Submodule
在父项目中的变动:
1 | cd .. |
可以看到pod-library
中已经变更为Submodule
最新的commit id
:
1 | Subproject commit 330417cf3fc1d2c42092b20506b0d296d90d0b5f |
需要把Submodule
的变动信息推送到父项目的远程服务器
1 | git commit -m'update submodule' |
这样就把子模块的变更信息以及子模块的变更信息提交到远程服务器了,从远程服务器上更新下来的内容就是最新提交的内容了。
更新Submodule
有两种方式:
在父项目的目录下直接运行
1 | git submodule foreach git pull |
在Submodule的目录下面更新
1 | cd pod-library |
可以看到在Submodule
的目录中,使用git
和单独的一个项目是一样的,注意更新Submodule
的时候如果有新的commit id
产生,需要在父项目产生一个新的提交,pod-libray文件中的 Subproject commit
会变为最新的commit id
。
clone Submodule
有两种方式 一种是采用递归的方式clone整个项目,一种是clone父项目,再更新子项目。
--recursive
1 | git clone git@github.com:xxx.git --recursive |
输出结果:
1 | loning into 'pod-project'... |
可以看到init Submodule
会自动被clone
下来
Submodule
1 | git clone git@github.com:xxx/pod-project.git |
输出:
1 | Submodule 'pod-library' (git@github.com:xxx/pod-library.git) |
更新Submodule
:
1 | git submodule update |
运行结果:
1 | Cloning into 'pod-library'... |
git
并不支持直接删除Submodule
需要手动删除对应的文件:
1 | cd pod-project |
更改git的配置文件config
:
1 | vim .git/config |
可以看到Submodule
的配置信息:
1 | [submodule "pod-library"] |
删除submodule相关的内容,然后提交到远程服务器:
1 | git commit -a -m 'remove pod-library submodule' |
否则,每个人都提交一堆杂乱无章的commit,项目很快就会变得难以协调和维护。
下面是ThoughtBot 的Git使用规范流程。我从中学到了很多,推荐你也这样使用Git。
首先,每次开发新功能,都应该新建一个单独的分支(这方面可以参考《Git分支管理策略》)。
# 获取主干最新代码$ git checkout master$ git pull# 新建一个开发分支myfeature$ git checkout -b myfeature
分支修改后,就可以提交commit了。
$ git add --all$ git status$ git commit --verbose
git add
命令的all参数,表示保存所有变化(包括新建、修改和删除)。从Git 2.0开始,all是 git add 的默认参数,所以也可以用 git add . 代替。
git status
命令,用来查看发生变动的文件。
git commit
命令的 verbose
参数,会列出 diff 的结果。
提交commit时,必须给出完整扼要的提交信息,下面是一个范本。
Present-tense summary under 50 characters* More information about commit (under 72 characters).* More information about commit (under 72 characters).http://project.management-system.com/ticket/123
第一行是不超过50个字的提要,然后空一行,罗列出改动原因、主要变动、以及需要注意的问题。最后,提供对应的网址(比如Bug ticket)。
分支的开发过程中,要经常与主干保持同步。
$ git fetch origin$ git rebase origin/master
分支开发完成后,很可能有一堆 commit
,但是合并到主干的时候,往往希望只有一个(或最多两三个)commit
,这样不仅清晰,也容易管理。
那么,怎样才能将多个 commit
合并呢?这就要用到 git rebase
命令。
$ git rebase -i origin/master
git rebase
命令的 i
参数表示互动(interactive),这时git会打开一个互动界面,进行下一步操作。
pick 07c5abd Introduce OpenPGP and teach basic usagepick de9b1eb Fix PostChecker::Post#urlspick 3e7ee36 Hey kids, stop all the highlightingpick fa20af3 git interactive rebase, squash, amend# Rebase 8db7e8b..fa20af3 onto 8db7e8b## Commands:# p, pick = use commit# r, reword = use commit, but edit the commit message# e, edit = use commit, but stop for amending# s, squash = use commit, but meld into previous commit# f, fixup = like "squash", but discard this commit's log message# x, exec = run command (the rest of the line) using shell## These lines can be re-ordered; they are executed from top to bottom.## If you remove a line here THAT COMMIT WILL BE LOST.## However, if you remove everything, the rebase will be aborted.## Note that empty commits are commented out
上面的互动界面,先列出当前分支最新的4个 commit
(越下面越新)。每个 commit
前面有一个操作命令,默认是 pick
,表示该行 commit
被选中,要进行 rebase
操作。
4个commit的下面是一大堆注释,列出可以使用的命令。
上面这6个命令当中,squash
和 fixup
可以用来合并 commit
。先把需要合并的 commit
前面的动词,改成 squash
(或者s)。
pick 07c5abd Introduce OpenPGP and teach basic usages de9b1eb Fix PostChecker::Post#urlss 3e7ee36 Hey kids, stop all the highlightingpick fa20af3 git interactive rebase, squash, amend
这样一改,执行后,当前分支只会剩下两个commit。第二行和第三行的commit,都会合并到第一行的commit。提交信息会同时包含,这三个commit的提交信息。
# This is a combination of 3 commits.# The first commit's message is:Introduce OpenPGP and teach basic usage# This is the 2nd commit message:Fix PostChecker::Post#urls# This is the 3rd commit message:Hey kids, stop all the highlighting
如果将第三行的 squash
命令改成 fixup
命令。
pick 07c5abd Introduce OpenPGP and teach basic usages de9b1eb Fix PostChecker::Post#urlsf 3e7ee36 Hey kids, stop all the highlightingpick fa20af3 git interactive rebase, squash, amend
运行结果相同,还是会生成两个commit,第二行和第三行的commit,都合并到第一行的commit。但是,新的提交信息里面,第三行commit的提交信息,会被注释掉。
# This is a combination of 3 commits.# The first commit's message is:Introduce OpenPGP and teach basic usage# This is the 2nd commit message:Fix PostChecker::Post#urls# This is the 3rd commit message:# Hey kids, stop all the highlighting
Pony Foo提出另外一种合并commit的简便方法,就是先撤销过去5个commit,然后再建一个新的。
$ git reset HEAD~5$ git add .$ git commit -am "Here's the bug fix that closes #28"$ git push --force
squash
和 fixup
命令,还可以当作命令行参数使用,自动合并commit。
$ git commit --fixup $ git rebase -i --autosquash
这个用法请参考http://fle.github.io/git-tip-keep-your-branch-clean-with-fixup-and-autosquash.html,这里就不解释了。
合并commit后,就可以推送当前分支到远程仓库了。
$ git push --force origin myfeature
git push
命令要加上 force
参数,因为 rebase
以后,分支历史改变了,跟远程分支不一定兼容,有可能要强行推送。
提交到远程仓库以后,就可以发出 Pull Request
到 master
分支,然后请求别人进行代码 review
,确认可以合并到 master
。
一般来说,日常使用只要记住下图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。
下面是我整理的常用 Git 命令清单。几个专用名词的译名如下。
# 在当前目录新建一个Git代码库$ git init# 新建一个目录,将其初始化为Git代码库$ git init [project-name]# 下载一个项目和它的整个代码历史$ git clone [url]
Git的设置文件为 .gitconfig
,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。
# 显示当前的Git配置$ git config --list# 编辑Git配置文件$ git config -e [--global]# 设置提交代码时的用户信息$ git config [--global] user.name "[name]"$ git config [--global] user.email "[email address]"
# 添加指定文件到暂存区$ git add [file1] [file2] ...# 添加指定目录到暂存区,包括子目录$ git add [dir]# 添加当前目录的所有文件到暂存区$ git add .# 添加每个变化前,都会要求确认# 对于同一个文件的多处变化,可以实现分次提交$ git add -p# 删除工作区文件,并且将这次删除放入暂存区$ git rm [file1] [file2] ...# 停止追踪指定文件,但该文件会保留在工作区$ git rm --cached [file]# 改名文件,并且将这个改名放入暂存区$ git mv [file-original] [file-renamed]
# 提交暂存区到仓库区$ git commit -m [message]# 提交暂存区的指定文件到仓库区$ git commit [file1] [file2] ... -m [message]# 提交工作区自上次commit之后的变化,直接到仓库区$ git commit -a# 提交时显示所有diff信息$ git commit -v# 使用一次新的commit,替代上一次提交# 如果代码没有任何新变化,则用来改写上一次commit的提交信息$ git commit --amend -m [message]# 重做上一次commit,并包括指定文件的新变化$ git commit --amend [file1] [file2] ...
# 列出所有本地分支$ git branch# 列出所有远程分支$ git branch -r# 列出所有本地分支和远程分支$ git branch -a# 新建一个分支,但依然停留在当前分支$ git branch [branch-name]# 新建一个分支,并切换到该分支$ git checkout -b [branch]# 新建一个分支,指向指定commit$ git branch [branch] [commit]# 新建一个分支,与指定的远程分支建立追踪关系$ git branch --track [branch] [remote-branch]# 切换到指定分支,并更新工作区$ git checkout [branch-name]# 切换到上一个分支$ git checkout -# 建立追踪关系,在现有分支与指定的远程分支之间$ git branch --set-upstream [branch] [remote-branch]# 合并指定分支到当前分支$ git merge [branch]# 选择一个commit,合并进当前分支$ git cherry-pick [commit]# 删除分支$ git branch -d [branch-name]# 删除远程分支$ git push origin --delete [branch-name]$ git branch -dr [remote/branch]
# 列出所有tag$ git tag# 新建一个tag在当前commit$ git tag [tag]# 新建一个tag在指定commit$ git tag [tag] [commit]# 删除本地tag$ git tag -d [tag]# 删除远程tag$ git push origin :refs/tags/[tagName]# 查看tag信息$ git show [tag]# 提交指定tag$ git push [remote] [tag]# 提交所有tag$ git push [remote] --tags# 新建一个分支,指向某个tag$ git checkout -b [branch] [tag]
# 显示有变更的文件$ git status# 显示当前分支的版本历史$ git log# 显示commit历史,以及每次commit发生变更的文件$ git log --stat# 搜索提交历史,根据关键词$ git log -S [keyword]# 显示某个commit之后的所有变动,每个commit占据一行$ git log [tag] HEAD --pretty=format:%s# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件$ git log [tag] HEAD --grep feature# 显示某个文件的版本历史,包括文件改名$ git log --follow [file]$ git whatchanged [file]# 显示指定文件相关的每一次diff$ git log -p [file]# 显示过去5次提交$ git log -5 --pretty --oneline# 显示所有提交过的用户,按提交次数排序$ git shortlog -sn# 显示指定文件是什么人在什么时间修改过$ git blame [file]# 显示暂存区和工作区的差异$ git diff# 显示暂存区和上一个commit的差异$ git diff --cached [file]# 显示工作区与当前分支最新commit之间的差异$ git diff HEAD# 显示两次提交之间的差异$ git diff [first-branch]...[second-branch]# 显示今天你写了多少行代码$ git diff --shortstat "@{0 day ago}"# 显示某次提交的元数据和内容变化$ git show [commit]# 显示某次提交发生变化的文件$ git show --name-only [commit]# 显示某次提交时,某个文件的内容$ git show [commit]:[filename]# 显示当前分支的最近几次提交$ git reflog
# 下载远程仓库的所有变动$ git fetch [remote]# 显示所有远程仓库$ git remote -v# 显示某个远程仓库的信息$ git remote show [remote]# 增加一个新的远程仓库,并命名$ git remote add [shortname] [url]# 取回远程仓库的变化,并与本地分支合并$ git pull [remote] [branch]# 上传本地指定分支到远程仓库$ git push [remote] [branch]# 强行推送当前分支到远程仓库,即使有冲突$ git push [remote] --force# 推送所有分支到远程仓库$ git push [remote] --all
# 恢复暂存区的指定文件到工作区$ git checkout [file]# 恢复某个commit的指定文件到暂存区和工作区$ git checkout [commit] [file]# 恢复暂存区的所有文件到工作区$ git checkout .# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变$ git reset [file]# 重置暂存区与工作区,与上一次commit保持一致$ git reset --hard# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变$ git reset [commit]# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致$ git reset --hard [commit]# 重置当前HEAD为指定commit,但保持暂存区和工作区不变$ git reset --keep [commit]# 新建一个commit,用来撤销指定commit# 后者的所有变化都将被前者抵消,并且应用到当前分支$ git revert [commit]# 暂时将未提交的变化移除,稍后再移入$ git stash$ git stash pop
# 生成一个可供发布的压缩包$ git archive
]]>1 | # 开启Docker守护进程调试模式 |
创建守护式容器
1 | $ sudo docker run --name daemon_dave -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done" |
上面的docker run 使用了
-d
参数,因此Docker会将容器放到后台运行。
Docker日志
1 | # 获取守护式容器的日志 |
Docker日志驱动
1 | $ sudo docker run --log-driver="syslog" --name daemon_dave -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done" |
使用syslog将会禁用docker logs命令,并且将所有容器的日志输出都重定向到Syslog。
查看容器内的进程
1 | $ sudo docker top daemon_dave |
Docker统计信息
1 | $ sudo docker stats daemon_dave daemon_kate daemon_clear daemon_sarah |
以上命令可以看到一个守护容器的列表,以及他们的CPU、内存、网络I/O以及存储I/O的性能和指标。这对快速监控一台主机上的一组容器非常有用。
在容器内部运行进程
1 | $ sudo docker exec -d daemon_dave touch /etc/new_config_file |
-d
表示需要运行一个后台进程
1 | # 在容器内运行交互命令 |
自动重启容器
1 | $ sudo docker run --restart=always --name daemon_dave -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done" |
--restart
标志被设置为always。无论容器的退出代码是什么,Docker都会自动重启改容器。除了always,还可以将这个标志设为on-failure
,这样,只有当容器的退出代码为非0值的时候,才会自动重启。另外,on-failure还接受一个可选的重启次数参数,--restart=on-failure:5
,Docker会尝试自动重启改容器,最多重启5次。
深入容器
1 | $ sudo docker inspect daemon_dave |
docker inspect命令会对容器进行详细的检查,然后返回其配置信息,包括名称、命令、网络配置以及很多有用的数据。可以使用
-f
或者--format
标志来选定查看结果。
1 | $ sudo docker inspect --format='{.State.Running}' daemon_dave |
查看多个容器
1 | $ sudo docker inspect --format '{.Name} {.State.Running}' daemon_dave Micheal_container |
删除容器
1 | $ sudo docker rm daemon_dave |
列出所有镜像
1 | $ sudo docker images |
拉去镜像
1 | $ sudo docker pull ubuntu:16.04 |
运行一个带标签的Docker镜像
1 | $ sudo docker run -i -t --name new_container ubuntu:16.04 /bin/bash |
查找镜像
1 | $ sudo docker search puppet |
构建镜像
docker commit
命令docker build
命令和Dockerfile
文件用Docker的commit命令创建镜像
1 | $ sudo docker run -i -t ubuntu /bin/bash |
用Dockerfile构建镜像
Dockerfile文件示例:
1 | Vsersion: 0.0.1 |
Dockerfile中的指令会按照顺序从上到下执行,所以根据需要合理安排指令的顺序。
如果Dockerfile由于某些原因没有正常结束,那么用户得到了一个可以使用的镜像。这对调试非常有帮助:可以基于改镜像运行一个具备交互功能的容器,使用最后创建的镜像对为什么用户指令会失败进行调试。
每个Dockerfile的第一条指令必须是FROM,FROM指令指定一个已经存在的镜像,后续指令都将基于该镜像进行,这个镜像被称为基础镜像。
MAINTAINER指令告诉Docker镜像的作者是谁,以及作者的电子邮件。有助于标识镜像的所有者和联系方式。
默认情况下,RUN指令会在shell里使用命令包装器
/bin/sh -c
来执行,如果是在一个不支持shell的平台上运行或者不希望在shell中运行(比如避免shell字符串篡改),也可以使用exec
格式的RUN指令,如下所示:
1 | RUN [ "apt-get", " install", "-y", "nginx" ] |
EXPOSE指令告诉Docker该容器内的应用程序将会使用该容器的指定端口。
基于Dockerfile构建新镜像
1 | $ sudo docker build -t="micheal/static_web" . |
查看镜像
1 | # 列出Docker镜像 |
nginx -g “daemon off;”,这将以前台的方式启动Nginx。
-p
标志用来控制Docker在运行时应该公开那些网络端口给外部(宿主机)。运行一个容器时,Docker可以通过两种方式来在宿主机上分配端口。
- Docker可以在宿主机上随机选择一个位于32768 ~ 61000的一个比较大的端口号来映射到容器中的80端口上。
- 可以在Docker宿主机只指定一个具体的端口号来映射到容器中的80端口上。
查看Docker端口映射情况
1 | $ sudo docker ps -l |
Dockerfile指令
CMD指令用于指定一个容器启动时要运行的命令。这有点儿类似于RUN指令,只是RUN指令是指定容器镜像被构建时要运行的命令,而CMD是指定容器被启动时要运行的命令。
1 | CMD ["/bin/bash/", "-l"] |
ENTRYPOINT和CMD指令非常类似,我们可在docker run命令行中覆盖CMD指令,而ENTRYPOINT指令提供的命令则不容易在启动容器的时候被覆盖。
可以组合使用ENTRYPOINT和CMD指令来完成一些巧妙的工作。
1 | ENTRYPOINT ["/usr/sbin/nginx"] |
WORKDIR指令用来在从镜像创建一个新容器时,在容器内部设置一个工作目录,ENTRYPOINT和/或CMD指定的程序会在这个目录下执行。
1 | WORKDIR /opt/webapp/db |
可以通过
-w
标志在运行时覆盖工作目录
1 | $ sudo docker run -ti -w /var/log ubuntu pwd/var/log |
ENV指令用来在镜像构建过程中设置环境变量。这些变量会持久保存到从我们镜像创建的任何容器中。
1 | ENV RVM_PATH /home/rvm |
也可以使用docker run命令行的
-e
标志来传递环境变量。这些环境变量只会在运行时有效。
1 | $ sudo docker run -ti -e "WEB_PORT=8080" ubuntu env |
USER指令用来指定该镜像会以什么样的用户身份来运行。我们可以指定用户名或者UID以及组或GID,甚至是两者的组合。
1 | USER user |
也可以在docker run命令行中通过
-u
标志覆盖该指令指定的值。
VOLUME指令用来向基于镜像创建的容器添加卷。一个卷可以存在于一个或者多个容器内特定的目录,这个目录可以绕过联合文件系统,并提供如下共享数据或者对数据进行持久化的功能。
- 卷可以在容器间共享和重用
- 一个容器可以不是必须和其他容器共享卷
- 对卷的修改是立即生效的
- 对卷的修改不会对更新镜像产生影响
- 卷会一直存在直到没有任何容器再使用它
卷功能让我们可以将数据(如源代码)、数据库或者其他内容添加到镜像中而不是将这些内容提交到镜像中,并且允许我们在多个容器间共享这些内容,我们可以利用此功能来测试容器和内部应用程序代码,管理日志,或者处理容器内部的数据库。
1 | VOLUME ["/opt/project"] |
这条指令将会基于此镜像的任何容器创建一个名为/opt/project的挂载点。
也可以通过指定数组的方式指定多个卷
1 | VOLUME ["/opt/project", "/data"] |
ADD指令用来将构建环境下的文件和目录复制到镜像中。不能对构建目录或者上下文之外的文件进行ADD操作。
1 | ADD software.lic /opt/application/software.lic |
COPY指令非常类似ADD,它们根本不同是COPY只关心构建上下文中复制本地文件,而不会去做文件提取(extraction)和解压(decompression)的工作。
1 | COPY conf.d/ /etc/apache2/ |
LABEL指令用于为Docker镜像添加元数据。元数据以键值对的形式展现
1 | LABEL version="1.0" |
可以使用docker inspect命令查看容器标签
1 | $ sudo docker inspect micheal/apache2 |
STOPSIGNAL指令用来设置停止容器时发送什么系统调用信号给容器。
ARG指令用来定义可以在docker build命令运行时传递给构建运行时的变量,我们只需要在构建时使用–build-arg标志即可。用户只能在构建时指定在Dockerfile文件汇总定义过的参数。
1 | ARG build |
ONBUILD指令能为镜像添加触发器(trigger)。当一个镜像被用做其他镜像的基础镜像时(比如用户的镜像需要从某未准备好的位置添加源代码,或者用户需要执行特定于构建镜像的环境的构建脚本),该镜像中的触发器将会被执行。
触发器会在构建过程中插入新指令,我们可以认为这些指令是紧跟在FROM之后指定的。触发器可以是任何构建指令。
1 | ONBUILD ADD . /app/src |
上面的代码将会在创建的镜像中加入ONBUILD触发器,ONBUILD指令可以在镜像上运行docker inspect命令查看。
Docker Networking
容器之间的连接用网络创建,这被称为Docker Networking。Docker Networking允许用户创建自己的网络,容器可以通过这个网上互相通信。更重要的是,现在容器可以跨越不同的宿主机来通信,并且网络配置可以更灵活的定制。Docker Networking也和Docker Compose以及Swarm进行了集成。
要想使用Docker网络,需要先创建一个网络,然后在这个网络下启动容器。
1 | $ sudo docker network create app |
这里使用docker network命令创建了一个桥接网络,命名为app。可以使用docker network inspect命令查看新创建的这个网络。
1 | $ sudo docker network inspect app |
我们可以看到这个新网络是一个本地的桥接网络(这非常像docker0网络),而且现在没有容器再这个网络中运行。
可以使用
docker network ls
命令列出当前系统中所有的网络。
1 | $ sudo docker network ls |
也可以使用
docker network rm
命令删除一个Docker网络。在Docker网络中创建Redis容器
1 | $ sudo docker run -d --net=app --name db micheal/redis |
--net
标志指定了新容器将会在那个网络中运行。
1 | $ sudo docker network inspect app |
将已有容器连接到Docker网络
1 | $ sudo docker network connect app db2 |
可以通过
docker network disconnect
命令断开一个容器与指定网络的连接
1 | $ sudo docker network disconnect app db2 |
通过Docker链接连接容器
启动一个Redis容器
1 | $ sudo docker run -d --name redis micheal/redis |
注意:这里没有公开容器的任何端口。一会就能看到这么做的原因。
链接Redis容器
1 | $ sudo docker run -p 4567 --name webapp --link redis:db -t -i -v $PWD/webapp_redis:/opt/webapp micheal/sinatra /bin/bash |
这个命令做了不少事情,我们逐一解释。首先,我们使用
-p
标志公开4567端口,这样就能从外面访问web应用程序。我们还使用
--name
标志给容器命名为webapp,并且使用了-v
标志把web应用程序目录作为卷挂载到了容器里。然而,这次我们使用了一个新标志
--link
。--link
标志创建了两个容器间的客户-服务链接。这个标志需要两个参数:一个是要链接的容器的名字,另一个是链接的别名。这个例子中我们创建了客户联系,webapp容器是客户,redis容器是“服务”,并且为这个服务增加了db作为别名。这个别名让我们可以一致地访问容器公开信息,而无须关注底层容器的名字。链接让服务容器有能力与客户容器通信,并且能分享一些连接细节,这些细节有助于在应用程序中配置并使用这个链接。
]]>连接也能得到一些安全上的好处。注意,启动 Redis 容器时,并没有使用
-p
标志公开Redis的端口。因为不需要这么做。通过把容器链接在一起,可以让客户直接访问任意服务容器的公开端口(即客户webapp容器可以连接到服务redis容器的6379端口)。更妙的是,只有使用--link
标志链接到这个容器的容器才能连接到这个端口。容器的端口不需要对本地宿主机公开,现在我们已经拥有一个非常安全的模型。通过这个安全模型,就可以限制容器化应用程序被攻击面,减少应用暴露的网络。
Hexo + GitHub (Coding) Pages 搭建博客
Hexo的Next主题个性化设置(一)——基础设置
]]>【设计模式】C++设计模式(全26讲)
设计模式的软件处理的核心:抽象稳定,隔离变化。实现可扩展、灵活、低耦合、稳定的框架。
目标:
“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。——Christopher Alexander
底层思维:向下,如何把握机器底层从微观理解对象构造
语言构造
编译转换
内存模型
运行时机制
抽象思维:向上,如何将我们的周围世界抽象为程序代码
面向对象
组件封装
设计模式
架构模式
向下:深入理解三大面向对象机制
封装,隐藏内部实现
继承,复用现有代码
多态,改写对象行为
向上:深刻把握面向对象机制所带来的抽象意义,理解如何使用这些机制来表达现实世界,掌握什么是“好的面向对象设计”
建筑商从来不会去想给一栋已建好的100层高的楼房底下再新修一个小地下室——这样做花费极大而且注定要失败。然而令人惊奇的是,软件系统的用户在要求作出类似改变时却不会仔细考虑,而且他们认为这只是需要简单编程的事。 ——Object-Oriented Analysis and Design with Applications
变化
分解
抽象
结构化 VS. 面向对象
什么是好的软件设计?软件设计的金科玉律:复用!
变化是复用的天敌!面向对象设计最大的优势在于:抵御变化!
理解隔离变化
各司其职
从微观层面来看,面向对象的方式更强调各个类的“责任”
由于需求变化导致的新增类型不应该影响原来类型的实现——是所谓各负其责
对象是什么?
从语言实现层面来看,对象封装了代码和数据。
从规格层面讲,对象是一系列可被使用的公共接口。
从概念层面讲,对象是某种拥有责任的抽象。
依赖倒置原则(DIP)
高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。
抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。
开放封闭原则(OCP)
对扩展开放,对更改封闭。
类模块应该是可扩展的,但是不可修改。
单一职责原则(SRP)
一个类应该仅有一个引起它变化的原因。
变化的方向隐含着类的责任。
Liskov 替换原则(LSP)
子类必须能够替换它们的基类(IS-A)。
继承表达类型抽象。
接口隔离原则(ISP)
不应该强迫客户程序依赖它们不用的方法。
接口应该小而完备。
优先使用对象组合,而不是类继承
类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。
继承在某种程度上破坏了封装性,子类父类耦合度高。
而对象组合则只要求被组合的对象具有良好定义的接口,耦合 度低。
封装变化点
针对接口编程,而不是针对实现编程
不将变量类型声明为某个特定的具体类,而是声明为某个接口。
客户程序无需获知对象的具体类型,只需要知道对象所具有的 接口。
减少系统中各部分的依赖关系,从而实现“高内聚、松耦合” 的类型设计方案。
产业强盛的标志:接口标准化!
设计习语 Design Idioms
设计模式 Design Patterns
架构模式 Architectural Patterns
从目的来看:
创建型(Creational)模式:将对象的部分创建工作延迟到子 类或者其他对象,从而应对需求变化为对象创建时具体类型实 现引来的冲击。
结构型(Structural)模式:通过类继承或者对象组合获得更灵 活的结构,从而应对需求变化为对象的结构带来的冲击。
行为型(Behavioral)模式:通过类继承或者对象组合来划分 类与对象间的职责,从而应对需求变化为多个交互的对象带来 的冲击。
从范围来看:
类模式处理类与子类的静态关系。
对象模式处理对象间的动态关系。
组件协作:
Template Method – 模板方法
Observer / Event – 观察者模式
Strategy – 策略模式
单一职责:
Decorator – 装饰器模式
Bridge – 桥接模式
对象创建:
Factory Method – 工厂方法
Abstract Factory – 抽象工厂
Prototype – 原型模式
Builder – 构建器模式 – 目前已经用的不多
对象性能:
Singleton – 单件模式
Flyweight – 享元模式
接口隔离:
Façade – 门面模式
Proxy – 代理模式
Mediator – 中介者模式 – 目前已经用的不多
Adapter – 适配器模式
状态变化:
Memento – 备忘录模式 – 目前已经用的不多
State – 状态模式
数据结构:
Composite – 组合模式
Iterator – 迭代器模式 – 目前已经用的不多
Chain of Resposibility – 职责链模式 – 目前已经用的不多
行为变化:
Command – 命令行模式 – 目前已经用的不多
Visitor – 访问器模式 – 目前已经用的不多
领域问题:
面向对象设计模式是“好的面向对象设计”,所谓“好的面向对 象设计”指是那些可以满足 “应对变化,提高复用”的设计 。
现代软件设计的特征是“需求的频繁变化”。设计模式的要点是 “寻找变化点,然后在变化点处应用设计模式,从而来更好地应对 需求的变化”.“什么时候、什么地点应用设计模式”比“理解设计模式结构本身”更为重要。
设计模式的应用不宜先入为主,一上来就使用设计模式是对设计 模式的最大误用。没有一步到位的设计模式。敏捷软件开发实践提倡的“Refactoring to Patterns”是目前普遍公认的最好的使用设计模式的方法。
静态 –> 动态
早绑定 –> 晚绑定
继承 –> 组合
编译时依赖 –> 运行时依赖
紧耦合 –> 松耦合
在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的原因 (比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求?
定义一个操作中的算法的骨架 (稳定),而将一些步骤延迟 (变化) 到子类中。Template Method使得子类可以不改变 (复用) 一个算法的结构即可重定义(override 重写)该算法的某些特定步骤。 ——《设计模式》GoF
1 |
在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。
如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?
定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(变化)。该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子类化)。 ——《设计模式》GoF
1 |
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
定义对象间的一种一对多(变化)的依赖关系,以便当一个 对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。 ——《设计模式》GoF
1 |
在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任。
典型模式
在某些情况下我们可能会“过度地使用继承来扩展对象的功能”,由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。
如何使“对象功能的扩展”能够根据需要来动态地实现?同时避免“扩展功能的增多”带来的子类膨胀问题?从而使得任何“功能扩展变化”所导致的影响将为最低?
动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码 & 减少子类个数)。 ——《设计模式》GoF
1 |
在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任。
典型模式
由于某些类型的固有的实现逻辑,使得它们具有两个变化的维度, 乃至多个纬度的变化。
如何应对这种“多维度的变化”?如何利用面向对象技术来使得类型可以轻松地沿着两个乃至多个方向变化,而不引入额外的复杂度?
将抽象部分(业务功能)与实现部分(平台实现)分离,使它们都可以独立地变化。 ——《设计模式》GoF
Bridge模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自纬度的变化,即“子类化”它们。
Bridge模式有时候类似于多继承方案,但是多继承方案往往违背单一职责原则(即一个类只有一个变化的原因),复用性比较差。Bridge模式是比多继承方案更好的解决方法。
Bridge模式的应用一般在“两个非常强的变化维度”,有时一个类也有多于两个的变化维度,这时可以使用Bridge的扩展模式。
1 |
通过“对象创建” 模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
典型模式
在软件系统中,经常面临着创建对象的工作;由于需求的变化,需要创建的对象的具体类型经常变化。
如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“具体对象创建工作”的紧耦合?
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使得一个类的实例化延迟(目的:解耦,手段:虚函数)到子类。 ——《设计模式》GoF
Factory Method模式用于隔离类对象的使用者和具体类型之间的耦合关系。面对一个经常变化的具体类型,紧耦合关系(new)会导致软件的脆弱。
Factory Method模式通过面向对象的手法,将所要创建的具体对象工作延迟到子类,从而实现一种扩展(而非更改)的策略,较好地解决了这种紧耦合关系。
Factory Method模式解决“单个对象”的需求变化。缺点在于要求创建方法/参数相同。
1 |
通过“对象创建” 模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
典型模式
在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时,由于需求的变化,往往存在更多系列对象的创建工作。
如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“多系列具体对象创建工作” 的紧耦合?
提供一个接口,让该接口负责创建一系列“相关或者相互依赖的对象”,无需指定它们具体的类。 ——《设计模式》GoF
如果没有应对“多系列对象构建”的需求变化,则没有必要使用 Abstract Factory模式,这时候使用简单的工厂完全可以。
“系列对象”指的是在某一特定系列下的对象之间有相互依赖、或作用的关系。不同系列的对象之间不能相互依赖。
Abstract Factory模式主要在于应对“新系列”的需求变动。其缺点在于难以应对“新对象”的需求变动。
1 |
通过“对象创建” 模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
典型模式
使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象。——《设计模式》GoF
1 |
通过“对象创建” 模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
典型模式
在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
如何应对这种变化?如何提供一种“封装机制”来隔离出“复杂对象的各个部分”的变化,从而保持系统中的“稳定构建算法”不随着需求改变而改变?
将一个复杂对象的构建与其表示相分离,使得同样的构建过程(稳定)可以创建不同的表示(变化)。 ——《设计模式》GoF
Builder 模式主要用于“分步骤构建一个复杂的对象”。在这其中 “分步骤”是一个稳定的算法,而复杂对象的各个部分则经常变化。
变化点在哪里,封装哪里—— Builder模式主要在于应对“复杂对象各个部分”的频繁需求变动。其缺点在于难以应对“分步骤构建算法”的需求变动。
在Builder模式中,要注意不同语言中构造器内调用虚函数的差别(C++ vs. C#) 。
1 |
保证一个类仅有一个实例,并提供一个该实例的全局访问点。——《设计模式》GoF
1 |
运用共享技术有效地支持大量细粒度的对象。——《设计模式》GoF
1 |
为子系统的一组接口提供一个一致(稳定)的界面,Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用(复用)。——《设计模式》GoF
1 |
为其他对象提供一种代理以控制(隔离,使用接口)对这个对象的访问。——《设计模式》GoF
1 |
将一个类的接口转换成客户希望的另一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。——《设计模式》GoF
1 |
用一个中介对象来封装(封装变化)一系列的对象交互。中介者使各对象不需要显式的相互引用(编译时依赖–>运行时依赖),从而使得其耦合松散(管理变化),而且可以独立地改变它们之间的交互。——《设计模式》GoF
1 |
允许一个对象在其内部状态改变时改变它的行为。从而使对象看起来似乎修改了其行为。——《设计模式》GoF
1 |
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。——《设计模式》GoF
1 |
将对象组合成树形结构以表示 “部分—整体” 的层次结构。Composite 使得用户对单个对象和组合对象的使用具有一致性(稳定)。——《设计模式》GoF
1 |
提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露(稳定)该对象的内部表示。——《设计模式》GoF
1 |
使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这些对象连城一条链,并沿着这条链传递请求,直到有一个对象处理它位置。——《设计模式》GoF
1 |
将一个请求(行为)封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。——《设计模式》GoF
1 |
表示一个作用于某对象结构中的各元素的操作。使得可以在不改变(稳定)各元素的类的前提下定义(扩展)作用于这些元素的新操作(变化)。——《设计模式》GoF
1 |
给定一个语言,定义它的文法的一种表示,并定义一种解释器,这个解释器使用该表示来解释语言中的句子。——《设计模式》GoF
1 |
管理变化,提高复用!
分解 vs. 抽象
- 依赖倒置原则(DIP)
- 开放封闭原则(OCP)
- 单一职责原则(SRP)
- Liskov 替换原则(LSP)
- 接口隔离原则(ISP)
- 对象组合优于类继承
- 封装变化点
- 面向接口编程
- 静态 –> 动态
- 早绑定 –> 晚绑定
- 继承 –> 组合
- 编译时依赖 –> 运行时依赖
- 紧耦合 –> 松耦合
- 代码可读性很差时
- 需求理解还很浅时
- 变化没有显现时
- 不是系统的关键依赖点
- 项目没有复用价值时
- 项目将要发布时
- 不要为模式而模式
- 关注抽象类 & 接口
- 理清变化点和稳定点
- 审视依赖关系
- 要有 Framework 和 Application 的区隔思维
- 良好的设计是演化的结果
- “手中无剑,心中无剑”:见模式而不知
- “手中有剑,心中无剑”:可以识别模式,作为应用开发人员使用模式
- “手中有剑,心中有剑”:作为框架开发人员为应用设计某些模式
- “手中无剑,心中有剑”:忘掉模式,只有原则
任何基类都要实现虚析构函数。 eg.
virtual ~Library(){}
将一些步骤延迟到子类中,其实就是定义一个虚函数让子类去实现或者重写这个函数。
虚函数和纯虚函数(纯虚函数子类必须实现,虚函数基类必须实现)
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。
定义纯虚函数就是为了让基类不可实例化化,因为实例化这样的抽象数据结构本身并没有意义。或者给出实现也没有意义
Template Method 机制 – 虚函数的多态性
看类图的时候要养成标注那些是稳定的那些是变化的。
扩展就是继承+多态
继承表明子类遵循基类定义的规范
]]>软件设计模式概述
虚函数和纯虚函数的区别
C++ 封装、继承、多态、重载、覆盖、隐藏基本概念详解
C++封装继承多态总结
手把手带你深入浅出神秘的设计模式
看懂UML类图和时序图
Jeff Dean其人:http://research.google.com/people/jeff/index.html,Google大规模分布式平台Bigtable和MapReduce主要设计和实现者。
Sanjay Ghemawat其人:http://research.google.com/people/sanjay/index.html,Google大规模分布式平台GFS,Bigtable和MapReduce主要设计和实现工程师。
LevelDb就是这两位大神级别的工程师发起的开源项目,简而言之,LevelDb是能够处理十亿级别规模Key-Value型数据持久性存储的C++ 程序库。正像上面介绍的,这二位是Bigtable的设计和实现者,如果了解Bigtable的话,应该知道在这个影响深远的分布式存储系统中有两个核心的部分:Master Server和Tablet Server。其中Master Server做一些管理数据的存储以及分布式调度工作,实际的分布式数据存储以及读写操作是由Tablet Server完成的,而LevelDb则可以理解为一个简化版的Tablet Server。
LevelDb有如下一些特点:
首先,LevelDb是一个持久化存储的KV系统,和Redis这种内存型的KV系统不同,LevelDb不会像Redis一样狂吃内存,而是将大部分数据存储到磁盘上。
其次,LevleDb在存储数据时,是根据记录的key值有序存储的,就是说相邻的key值在存储文件中是依次顺序存储的,而应用可以自定义key大小比较函数,LevleDb会按照用户定义的比较函数依序存储这些记录。
再次,像大多数KV系统一样,LevelDb的操作接口很简单,基本操作包括写记录,读记录以及删除记录。也支持针对多条操作的原子批量操作。
另外,LevelDb支持数据快照(snapshot)功能,使得读取操作不受写操作影响,可以在读操作过程中始终看到一致的数据。
除此外,LevelDb还支持数据压缩等操作,这对于减小存储空间以及增快IO效率都有直接的帮助。
LevelDb性能非常突出,官方网站报道其随机写性能达到40万条记录每秒,而随机读性能达到6万条记录每秒。总体来说,LevelDb的写操作要大大快于读操作,而顺序读写操作则大大快于随机读写操作。
LevelDb本质上是一套存储系统以及在这套存储系统上提供的一些操作接口。为了便于理解整个系统及其处理流程,我们可以从两个不同的角度来看待LevleDb:静态角度和动态角度。从静态角度,可以假想整个系统正在运行过程中(不断插入删除读取数据),此时我们给LevelDb照相,从照片可以看到之前系统的数据在内存和磁盘中是如何分布的,处于什么状态等;从动态的角度,主要是了解系统是如何写入一条记录,读出一条记录,删除一条记录的,同时也包括除了这些接口操作外的内部操作比如compaction,系统运行时崩溃后如何恢复系统等等方面。
本节所讲的整体架构主要从静态角度来描述,之后接下来的几节内容会详述静态结构涉及到的文件或者内存数据结构,LevelDb剖析后半部分主要介绍动态视角下的LevelDb,就是说整个系统是怎么运转起来的。
LevelDb作为存储系统,数据记录的存储介质包括内存以及磁盘文件,如果像上面说的,当LevelDb运行了一段时间,此时我们给LevelDb进行透视拍照,那么您会看到如下一番景象:
从图中可以看出,构成LevelDb静态结构的包括六个主要部分:内存中的MemTable和Immutable MemTable以及磁盘上的几种主要文件:Current文件,Manifest文件,log文件以及SSTable文件。当然,LevelDb除了这六个主要部分还有一些辅助的文件,但是以上六个文件和数据结构是LevelDb的主体构成元素。
LevelDb的Log文件和Memtable与Bigtable论文中介绍的是一致的,当应用写入一条Key:Value记录的时候,LevelDb会先往log文件里写入,成功后将记录插进Memtable中,这样基本就算完成了写入操作,因为一次写入操作只涉及一次磁盘顺序写和一次内存写入,所以这是为何说LevelDb写入速度极快的主要原因。
Log文件在系统中的作用主要是用于系统崩溃恢复而不丢失数据,假如没有Log文件,因为写入的记录刚开始是保存在内存中的,此时如果系统崩溃,内存中的数据还没有来得及Dump到磁盘,所以会丢失数据(Redis就存在这个问题)。为了避免这种情况,LevelDb在写入内存前先将操作记录到Log文件中,然后再记入内存中,这样即使系统崩溃,也可以从Log文件中恢复内存中的Memtable,不会造成数据的丢失。
当Memtable插入的数据占用内存到了一个界限后,需要将内存的记录导出到外存文件中,LevleDb会生成新的Log文件和Memtable,原先的Memtable就成为Immutable Memtable,顾名思义,就是说这个Memtable的内容是不可更改的,只能读不能写入或者删除。新到来的数据被记入新的Log文件和Memtable,LevelDb后台调度会将Immutable Memtable的数据导出到磁盘,形成一个新的SSTable文件。SSTable就是由内存中的数据不断导出并进行Compaction操作后形成的,而且SSTable的所有文件是一种层级结构,第一层为Level 0,第二层为Level 1,依次类推,层级逐渐增高,这也是为何称之为LevelDb的原因。
SSTable中的文件是Key有序的,就是说在文件中小key记录排在大Key记录之前,各个Level的SSTable都是如此,但是这里需要注意的一点是:Level 0的SSTable文件(后缀为.sst)和其它Level的文件相比有特殊性:这个层级内的.sst文件,两个文件可能存在key重叠,比如有两个level 0的sst文件,文件A和文件B,文件A的key范围是:{bar, car},文件B的Key范围是{blue,samecity},那么很可能两个文件都存在key=”blood”的记录。对于其它Level的SSTable文件来说,则不会出现同一层级内.sst文件的key重叠现象,就是说Level L中任意两个.sst文件,那么可以保证它们的key值是不会重叠的。这点需要特别注意,后面您会看到很多操作的差异都是由于这个原因造成的。
SSTable中的某个文件属于特定层级,而且其存储的记录是key有序的,那么必然有文件中的最小key和最大key,这是非常重要的信息,LevelDb应该记下这些信息。Manifest就是干这个的,它记载了SSTable各个文件的管理信息,比如属于哪个Level,文件名称叫啥,最小key和最大key各自是多少。下图是Manifest所存储内容的示意:
图中只显示了两个文件(manifest会记载所有SSTable文件的这些信息),即Level 0的test.sst1和test.sst2文件,同时记载了这些文件各自对应的key范围,比如test.sstt1
的key范围是“an”到 “banana”,而文件test.sst2的key范围是“baby”到“samecity”,可以看出两者的key范围是有重叠的。
Current文件是干什么的呢?这个文件的内容只有一个信息,就是记载当前的manifest文件名。因为在LevleDb的运行过程中,随着Compaction的进行,SSTable文件会发生变化,会有新的文件产生,老的文件被废弃,Manifest也会跟着反映这种变化,此时往往会新生成Manifest文件来记载这种变化,而Current则用来指出哪个Manifest文件才是我们关心的那个Manifest文件。
以上介绍的内容就构成了LevelDb的整体静态结构,在LevelDb剖析接下来的内容中,我们会首先介绍重要文件或者内存数据的具体数据布局与结构。
上节内容讲到log文件在LevelDb中的主要作用是系统故障恢复时,能够保证不会丢失数据。因为在将记录写入内存的Memtable之前,会先写入Log文件,这样即使系统发生故障,Memtable中的数据没有来得及Dump到磁盘的SSTable文件,LevelDB也可以根据log文件恢复内存的Memtable数据结构内容,不会造成系统丢失数据,在这点上LevelDb和Bigtable是一致的。
下面我们带大家看看log文件的具体物理和逻辑布局是怎样的,LevelDb对于一个log文件,会把它切割成以32K为单位的物理Block,每次读取的单位以一个Block作为基本读取单位,下图展示的log文件由3个Block构成,所以从物理布局来讲,一个log文件就是由连续的32K大小Block构成的。
在应用的视野里是看不到这些Block的,应用看到的是一系列的Key:Value对,在LevelDb内部,会将一个Key:Value对看做一条记录的数据,另外在这个数据前增加一个记录头,用来记载一些管理信息,以方便内部处理
记录头包含三个字段,ChechSum是对“类型”和“数据”字段的校验码,为了避免处理不完整或者是被破坏的数据,当LevelDb读取记录数据时候会对数据进行校验,如果发现和存储的CheckSum相同,说明数据完整无破坏,可以继续后续流程。“记录长度”记载了数据的大小,“数据”则是上面讲的Key:Value数值对,“类型”字段则指出了每条记录的逻辑结构和log文件物理分块结构之间的关系,具体而言,主要有以下四种类型:FULL/FIRST/MIDDLE/LAST。
如果记录类型是FULL,代表了当前记录内容完整地存储在一个物理Block里,没有被不同的物理Block切割开;如果记录被相邻的物理Block切割开,则类型会是其他三种类型中的一种。我们以图3.1所示的例子来具体说明。
假设目前存在三条记录,Record A,Record B和Record C,其中Record A大小为10K,Record B 大小为80K,Record C大小为12K,那么其在log文件中的逻辑布局会如图3.1所示。Record A是图中蓝色区域所示,因为大小为10K<32K,能够放在一个物理Block中,所以其类型为FULL;Record B 大小为80K,而Block 1因为放入了Record A,所以还剩下22K,不足以放下Record B,所以在Block 1的剩余部分放入Record B的开头一部分,类型标识为FIRST,代表了是一个记录的起始部分;Record B还有58K没有存储,这些只能依次放在后续的物理Block里面,因为Block 2大小只有32K,仍然放不下Record B的剩余部分,所以Block 2全部用来放Record B,且标识类型为MIDDLE,意思是这是Record B中间一段数据;Record B剩下的部分可以完全放在Block 3中,类型标识为LAST,代表了这是Record B的末尾数据;图中黄色的Record C因为大小为12K,Block 3剩下的空间足以全部放下它,所以其类型标识为FULL。
从这个小例子可以看出逻辑记录和物理Block之间的关系,LevelDb一次物理读取为一个Block,然后根据类型情况拼接出逻辑记录,供后续流程处理。
SSTable是Bigtable中至关重要的一块,对于LevelDb来说也是如此,对LevelDb的SSTable实现细节的了解也有助于了解Bigtable中一些实现细节。
本节内容主要讲述SSTable的静态布局结构,我们曾在“LevelDb剖析之二:整体架构”中说过,SSTable文件形成了不同Level的层级结构,至于这个层级结构是如何形成的我们放在后面Compaction一节细说。本节主要介绍SSTable某个文件的物理布局和逻辑布局结构,这对了解LevelDb的运行过程很有帮助。
LevelDb不同层级有很多SSTable文件(以后缀.sst为特征),所有.sst文件内部布局都是一样的。上节介绍Log文件是物理分块的,SSTable也一样会将文件划分为固定大小的物理存储块,但是两者逻辑布局大不相同,根本原因是:Log文件中的记录是Key无序的,即先后记录的key大小没有明确大小关系,而.sst文件内部则是根据记录的Key由小到大排列的,从下面介绍的SSTable布局可以体会到Key有序是为何如此设计.sst文件结构的关键。
上图展示了一个 .sst
文件的物理划分结构,同Log文件一样,也是划分为固定大小的存储块,每个Block分为三个部分,红色部分是数据存储区, 蓝色的Type区用于标识数据存储区是否采用了数据压缩算法(Snappy压缩或者无压缩两种),CRC部分则是数据校验码,用于判别数据是否在生成和传输中出错。
以上是 .sst
的物理布局,下面介绍.sst文件的逻辑布局,所谓逻辑布局,就是说尽管大家都是物理块,但是每一块存储什么内容,内部又有什么结构等。图4.2展示了 .sst
文件的内部逻辑解释。
从上图可以看出,从大的方面,可以将.sst文件划分为数据存储区和数据管理区,数据存储区存放实际的Key:Value数据,数据管理区则提供一些索引指针等管理数据,目的是更快速便捷的查找相应的记录。两个区域都是在上述的分块基础上的,就是说文件的前面若干块实际存储KV数据,后面数据管理区存储管理数据。管理数据又分为四种不同类型:紫色的Meta Block,红色的MetaBlock 索引和蓝色的数据索引块以及一个文件尾部块。
LevelDb 1.2版对于Meta Block尚无实际使用,只是保留了一个接口,估计会在后续版本中加入内容,下面我们看看数据索引区和文件尾部Footer的内部结构。
图4.3是数据索引的内部结构示意图。再次强调一下,Data Block内的KV记录是按照Key由小到大排列的,数据索引区的每条记录是对某个Data Block建立的索引信息,每条索引信息包含三个内容,以图4.3所示的数据块i的索引Index i来说:红色部分的第一个字段记载大于等于数据块i中最大的Key值的那个Key,第二个字段指出数据块i在.sst文件中的起始位置,第三个字段指出Data Block i的大小(有时候是有数据压缩的)。后面两个字段好理解,是用于定位数据块在文件中的位置的,第一个字段需要详细解释一下,在索引里保存的这个Key值未必一定是某条记录的Key,以图4.3的例子来说,假设数据块i 的最小Key=“samecity”,最大Key=“the best”;数据块i+1的最小Key=“the fox”,最大Key=“zoo”,那么对于数据块i的索引Index i来说,其第一个字段记载大于等于数据块i的最大Key(“the best”)同时要小于数据块i+1的最小Key(“the fox”),所以例子中Index i的第一个字段是:“the c”,这个是满足要求的;而Index i+1的第一个字段则是“zoo”,即数据块i+1的最大Key。
文件末尾Footer块的内部结构见图4.4,metaindex_handle指出了metaindex block的起始位置和大小;inex_handle指出了index Block的起始地址和大小;这两个字段可以理解为索引的索引,是为了正确读出索引值而设立的,后面跟着一个填充区和魔数。
上面主要介绍的是数据管理区的内部结构,下面我们看看数据区的一个Block的数据部分内部是如何布局的(图4.1中的红色部分),图4.5是其内部布局示意图。
从图中可以看出,其内部也分为两个部分,前面是一个个KV记录,其顺序是根据Key值由小到大排列的,在Block尾部则是一些“重启点”(Restart Point),其实是一些指针,指出Block内容中的一些记录位置。
“重启点”是干什么的呢?我们一再强调,Block内容里的KV记录是按照Key大小有序的,这样的话,相邻的两条记录很可能Key部分存在重叠,比如key i=“the Car”,Key i+1=“the color”,那么两者存在重叠部分“the c”,为了减少Key的存储量,Key i+1可以只存储和上一条Key不同的部分“olor”,两者的共同部分从Key i中可以获得。记录的Key在Block内容部分就是这么存储的,主要目的是减少存储开销。“重启点”的意思是:在这条记录开始,不再采取只记载不同的Key部分,而是重新记录所有的Key值,假设Key i+1是一个重启点,那么Key里面会完整存储“the color”,而不是采用简略的“olor”方式。Block尾部就是指出哪些记录是这些重启点的。
在Block内容区,每个KV记录的内部结构是怎样的?图4.6给出了其详细结构,每个记录包含5个字段:key共享长度,比如上面的“olor”记录, 其key和上一条记录共享的Key部分长度是“the c”的长度,即5;key非共享长度,对于“olor”来说,是4;value长度指出Key:Value中Value的长度,在后面的Value内容字段中存储实际的Value值;而key非共享内容则实际存储“olor”这个Key字符串。
上面讲的这些就是.sst文件的全部内部奥秘。
LevelDb剖析前述小节大致讲述了磁盘文件相关的重要静态结构,本小节讲述内存中的数据结构Memtable,Memtable在整个体系中的重要地位也不言而喻。总体而言,所有KV数据都是存储在Memtable,Immutable Memtable和SSTable中的,Immutable Memtable从结构上讲和Memtable是完全一样的,区别仅仅在于其是只读的,不允许写入操作,而Memtable则是允许写入和读取的。当Memtable写入的数据占用内存到达指定数量,则自动转换为Immutable Memtable,等待Dump到磁盘中,系统会自动生成新的Memtable供写操作写入新数据,理解了Memtable,那么Immutable Memtable自然不在话下。
LevelDb的MemTable提供了将KV数据写入,删除以及读取KV记录的操作接口,但是事实上Memtable并不存在真正的删除操作,删除某个Key的Value在Memtable内是作为插入一条记录实施的,但是会打上一个Key的删除标记,真正的删除操作是Lazy的,会在以后的Compaction过程中去掉这个KV。
需要注意的是,LevelDb的Memtable中KV对是根据Key大小有序存储的,在系统插入新的KV时,LevelDb要把这个KV插到合适的位置上以保持这种Key有序性。其实,LevelDb的Memtable类只是一个接口类,真正的操作是通过背后的SkipList来做的,包括插入操作和读取操作等,所以Memtable的核心数据结构是一个SkipList。
SkipList是由William Pugh发明。他在Communications of the ACM June 1990, 33(6) 668-676 发表了Skip lists: a probabilistic alternative to balanced trees,在该论文中详细解释了SkipList的数据结构和插入删除操作。
SkipList是平衡树的一种替代数据结构,但是和红黑树不相同的是,SkipList对于树的平衡的实现是基于一种随机化的算法的,这样也就是说SkipList的插入和删除的工作是比较简单的。
关于SkipList的详细介绍可以参考这篇文章,skip-list原理解析讲述的很清楚,LevelDb的SkipList基本上是一个具体实现,并无特殊之处。
SkipList不仅是维护有序数据的一个简单实现,而且相比较平衡树来说,在插入数据的时候可以避免频繁的树节点调整操作,所以写入效率是很高的,LevelDb整体而言是个高写入系统,SkipList在其中应该也起到了很重要的作用。Redis为了加快插入操作,也使用了SkipList来作为内部实现数据结构。
在之前的五节LevelDb剖析中,我们介绍了LevelDb的一些静态文件及其详细布局,从本节开始,我们看看LevelDb的一些动态操作,比如读写记录,Compaction,错误恢复等操作。
本节介绍levelDb的记录更新操作,即插入一条KV记录或者删除一条KV记录。levelDb的更新操作速度是非常快的,源于其内部机制决定了这种更新操作的简单性。
图6.1是levelDb如何更新KV数据的示意图,从图中可以看出,对于一个插入操作Put(Key,Value)来说,完成插入操作包含两个具体步骤:首先是将这条KV记录以顺序写的方式追加到之前介绍过的log文件末尾,因为尽管这是一个磁盘读写操作,但是文件的顺序追加写入效率是很高的,所以并不会导致写入速度的降低;第二个步骤是:如果写入log文件成功,那么将这条KV记录插入内存中的Memtable中,前面介绍过,Memtable只是一层封装,其内部其实是一个Key有序的SkipList列表,插入一条新记录的过程也很简单,即先查找合适的插入位置,然后修改相应的链接指针将新记录插入即可。完成这一步,写入记录就算完成了,所以一个插入记录操作涉及一次磁盘文件追加写和内存SkipList插入操作,这是为何levelDb写入速度如此高效的根本原因。
从上面的介绍过程中也可以看出:log文件内是key无序的,而Memtable中是key有序的。那么如果是删除一条KV记录呢?对于levelDb来说,并不存在立即删除的操作,而是与插入操作相同的,区别是,插入操作插入的是Key:Value 值,而删除操作插入的是“Key:删除标记”,并不真正去删除记录,而是后台Compaction的时候才去做真正的删除操作。
levelDb的写入操作就是如此简单。真正的麻烦在后面将要介绍的读取操作中。
LevelDb是针对大规模Key/Value数据的单机存储库,从应用的角度来看,LevelDb就是一个存储工具。而作为称职的存储工具,常见的调用接口无非是新增KV,删除KV,读取KV,更新Key对应的Value值这么几种操作。LevelDb的接口没有直接支持更新操作的接口,如果需要更新某个Key的Value,你可以选择直接生猛地插入新的KV,保持Key相同,这样系统内的key对应的value就会被更新;或者你可以先删除旧的KV, 之后再插入新的KV,这样比较委婉地完成KV的更新操作。
假设应用提交一个Key值,下面我们看看LevelDb是如何从存储的数据中读出其对应的Value值的。图7-1是LevelDb读取过程的整体示意图。
LevelDb首先会去查看内存中的Memtable,如果Memtable中包含key及其对应的value,则返回value值即可;如果在Memtable没有读到key,则接下来到同样处于内存中的Immutable Memtable中去读取,类似地,如果读到就返回,若是没有读到,那么只能万般无奈下从磁盘中的大量SSTable文件中查找。因为SSTable数量较多,而且分成多个Level,所以在SSTable中读数据是相当蜿蜒曲折的一段旅程。总的读取原则是这样的:首先从属于level 0的文件中查找,如果找到则返回对应的value值,如果没有找到那么到level 1中的文件中去找,如此循环往复,直到在某层SSTable文件中找到这个key对应的value为止(或者查到最高level,查找失败,说明整个系统中不存在这个Key)。
那么为什么是从Memtable到Immutable Memtable,再从Immutable Memtable到文件,而文件中为何是从低level到高level这么一个查询路径呢?道理何在?之所以选择这么个查询路径,是因为从信息的更新时间来说,很明显Memtable存储的是最新鲜的KV对;Immutable Memtable中存储的KV数据对的新鲜程度次之;而所有SSTable文件中的KV数据新鲜程度一定不如内存中的Memtable和Immutable Memtable的。对于SSTable文件来说,如果同时在level L和Level L+1找到同一个key,level L的信息一定比level L+1的要新。也就是说,上面列出的查找路径就是按照数据新鲜程度排列出来的,越新鲜的越先查找。
为啥要优先查找新鲜的数据呢?这个道理不言而喻,举个例子。比如我们先往levelDb里面插入一条数据 {key=”www.samecity.com" value=”我们”},过了几天,samecity网站改名为:69同城,此时我们插入数据{key=”www.samecity.com" value=”69同城”},同样的key,不同的value;逻辑上理解好像levelDb中只有一个存储记录,即第二个记录,但是在levelDb中很可能存在两条记录,即上面的两个记录都在levelDb中存储了,此时如果用户查询key=”www.samecity.com",我们当然希望找到最新的更新记录,也就是第二个记录返回,这就是为何要优先查找新鲜数据的原因。
前文有讲:对于SSTable文件来说,如果同时在level L和Level L+1找到同一个key,level L的信息一定比level L+1的要新。这是一个结论,理论上需要一个证明过程,否则会招致如下的问题:为神马呢?从道理上讲呢,很明白:因为Level L+1的数据不是从石头缝里蹦出来的,也不是做梦梦到的,那它是从哪里来的?Level L+1的数据是从Level L 经过Compaction后得到的(如果您不知道什么是Compaction,那么……..也许以后会知道的),也就是说,您看到的现在的Level L+1层的SSTable数据是从原来的Level L中来的,现在的Level L比原来的Level L数据要新鲜,所以可证,现在的Level L比现在的Level L+1的数据要新鲜。
SSTable文件很多,如何快速地找到key对应的value值?在LevelDb中,level 0一直都爱搞特殊化,在level 0和其它level中查找某个key的过程是不一样的。因为level 0下的不同文件可能key的范围有重叠,某个要查询的key有可能多个文件都包含,这样的话LevelDb的策略是先找出level 0中哪些文件包含这个key(manifest文件中记载了level和对应的文件及文件里key的范围信息,LevelDb在内存中保留这种映射表), 之后按照文件的新鲜程度排序,新的文件排在前面,之后依次查找,读出key对应的value。而如果是非level 0的话,因为这个level的文件之间key是不重叠的,所以只从一个文件就可以找到key对应的value。
最后一个问题,如果给定一个要查询的key和某个key range包含这个key的SSTable文件,那么levelDb是如何进行具体查找过程的呢?levelDb一般会先在内存中的Cache中查找是否包含这个文件的缓存记录,如果包含,则从缓存中读取;如果不包含,则打开SSTable文件,同时将这个文件的索引部分加载到内存中并放入Cache中。 这样Cache里面就有了这个SSTable的缓存项,但是只有索引部分在内存中,之后levelDb根据索引可以定位到哪个内容Block会包含这条key,从文件中读出这个Block的内容,在根据记录一一比较,如果找到则返回结果,如果没有找到,那么说明这个level的SSTable文件并不包含这个key,所以到下一级别的SSTable中去查找。
从之前介绍的LevelDb的写操作和这里介绍的读操作可以看出,相对写操作,读操作处理起来要复杂很多,所以写的速度必然要远远高于读数据的速度,也就是说,LevelDb比较适合写操作多于读操作的应用场合。而如果应用是很多读操作类型的,那么顺序读取效率会比较高,因为这样大部分内容都会在缓存中找到,尽可能避免大量的随机读取操作。
前文有述,对于LevelDb来说,写入记录操作很简单,删除记录仅仅写入一个删除标记就算完事,但是读取记录比较复杂,需要在内存以及各个层级文件中依照新鲜程度依次查找,代价很高。为了加快读取速度,levelDb采取了compaction的方式来对已有的记录进行整理压缩,通过这种方式,来删除掉一些不再有效的KV数据,减小数据规模,减少文件数量等。
levelDb的compaction机制和过程与Bigtable所讲述的是基本一致的,Bigtable中讲到三种类型的compaction: minor ,major和full。所谓minor Compaction,就是把memtable中的数据导出到SSTable文件中;major compaction就是合并不同层级的SSTable文件,而full compaction就是将所有SSTable进行合并。
LevelDb包含其中两种,minor和major。
我将为大家详细叙述其机理。
先来看看minor Compaction的过程。Minor compaction 的目的是当内存中的memtable大小到了一定值时,将内容保存到磁盘文件中,图8.1是其机理示意图。
从8.1可以看出,当memtable数量到了一定程度会转换为immutable memtable,此时不能往其中写入记录,只能从中读取KV内容。之前介绍过,immutable memtable其实是一个多层级队列SkipList,其中的记录是根据key有序排列的。所以这个minor compaction实现起来也很简单,就是按照immutable memtable中记录由小到大遍历,并依次写入一个level 0 的新建SSTable文件中,写完后建立文件的index 数据,这样就完成了一次minor compaction。从图中也可以看出,对于被删除的记录,在minor compaction过程中并不真正删除这个记录,原因也很简单,这里只知道要删掉key记录,但是这个KV数据在哪里?那需要复杂的查找,所以在minor compaction的时候并不做删除,只是将这个key作为一个记录写入文件中,至于真正的删除操作,在以后更高层级的compaction中会去做。
当某个level下的SSTable文件数目超过一定设置值后,levelDb会从这个level的SSTable中选择一个文件(level>0),将其和高一层级的level+1的SSTable文件合并,这就是major compaction。
我们知道在大于0的层级中,每个SSTable文件内的Key都是由小到大有序存储的,而且不同文件之间的key范围(文件内最小key和最大key之间)不会有任何重叠。Level 0的SSTable文件有些特殊,尽管每个文件也是根据Key由小到大排列,但是因为level 0的文件是通过minor compaction直接生成的,所以任意两个level 0下的两个sstable文件可能再key范围上有重叠。所以在做major compaction的时候,对于大于level 0的层级,选择其中一个文件就行,但是对于level 0来说,指定某个文件后,本level中很可能有其他SSTable文件的key范围和这个文件有重叠,这种情况下,要找出所有有重叠的文件和level 1的文件进行合并,即level 0在进行文件选择的时候,可能会有多个文件参与major compaction。
levelDb在选定某个level进行compaction后,还要选择是具体哪个文件要进行compaction,levelDb在这里有个小技巧, 就是说轮流来,比如这次是文件A进行compaction,那么下次就是在key range上紧挨着文件A的文件B进行compaction,这样每个文件都会有机会轮流和高层的level 文件进行合并。
如果选好了level L的文件A和level L+1层的文件进行合并,那么问题又来了,应该选择level L+1哪些文件进行合并?levelDb选择L+1层中和文件A在key range上有重叠的所有文件来和文件A进行合并。
也就是说,选定了level L的文件A,之后在level L+1中找到了所有需要合并的文件B,C,D…..等等。剩下的问题就是具体是如何进行major 合并的?就是说给定了一系列文件,每个文件内部是key有序的,如何对这些文件进行合并,使得新生成的文件仍然Key有序,同时抛掉哪些不再有价值的KV 数据。
图8.2说明了这一过程。
Major compaction的过程如下:对多个文件采用多路归并排序的方式,依次找出其中最小的Key记录,也就是对多个文件中的所有记录重新进行排序。之后采取一定的标准判断这个Key是否还需要保存,如果判断没有保存价值,那么直接抛掉,如果觉得还需要继续保存,那么就将其写入level L+1层中新生成的一个SSTable文件中。就这样对KV数据一一处理,形成了一系列新的L+1层数据文件,之前的L层文件和L+1层参与compaction 的文件数据此时已经没有意义了,所以全部删除。这样就完成了L层和L+1层文件记录的合并过程。
那么在major compaction过程中,判断一个KV记录是否抛弃的标准是什么呢?其中一个标准是:对于某个key来说,如果在小于L层中存在这个Key,那么这个KV在major compaction过程中可以抛掉。因为我们前面分析过,对于层级低于L的文件中如果存在同一Key的记录,那么说明对于Key来说,有更新鲜的Value存在,那么过去的Value就等于没有意义了,所以可以删除。
书接前文,前面讲过对于levelDb来说,读取操作如果没有在内存的memtable中找到记录,要多次进行磁盘访问操作。假设最优情况,即第一次就在level 0中最新的文件中找到了这个key,那么也需要读取2次磁盘,一次是将SSTable的文件中的index部分读入内存,这样根据这个index可以确定key是在哪个block中存储;第二次是读入这个block的内容,然后在内存中查找key对应的value。
levelDb中引入了两个不同的Cache:Table Cache和Block Cache。其中Block Cache是配置可选的,即在配置文件中指定是否打开这个功能。
图9.1是table cache的结构。在Cache中,key值是SSTable的文件名称,Value部分包含两部分,一个是指向磁盘打开的SSTable文件的文件指针,这是为了方便读取内容;另外一个是指向内存中这个SSTable文件对应的Table结构指针,table结构在内存中,保存了SSTable的index内容以及用来指示block cache用的cache_id ,当然除此外还有其它一些内容。
比如在get(key)读取操作中,如果levelDb确定了key在某个level下某个文件A的key range范围内,那么需要判断是不是文件A真的包含这个KV。此时,levelDb会首先查找Table Cache,看这个文件是否在缓存里,如果找到了,那么根据index部分就可以查找是哪个block包含这个key。如果没有在缓存中找到文件,那么打开SSTable文件,将其index部分读入内存,然后插入Cache里面,去index里面定位哪个block包含这个Key 。如果确定了文件哪个block包含这个key,那么需要读入block内容,这是第二次读取。
Block Cache是为了加快这个过程的,图9.2是其结构示意图。其中的key是文件的cache_id加上这个block在文件中的起始位置block_offset。而value则是这个Block的内容。
如果levelDb发现这个block在block cache中,那么可以避免读取数据,直接在cache里的block内容里面查找key的value就行,如果没找到呢?那么读入block内容并把它插入block cache中。levelDb就是这样通过两个cache来加快读取速度的。从这里可以看出,如果读取的数据局部性比较好,也就是说要读的数据大部分在cache里面都能读到,那么读取效率应该还是很高的,而如果是对key进行顺序读取效率也应该不错,因为一次读入后可以多次被复用。但是如果是随机读取,您可以推断下其效率如何。
Version 保存了当前磁盘以及内存中所有的文件信息,一般只有一个Version叫做”current” version(当前版本)。Leveldb还保存了一系列的历史版本,这些历史版本有什么作用呢?
当一个Iterator创建后,Iterator就引用到了current version(当前版本),只要这个Iterator不被delete那么被Iterator引用的版本就会一直存活。这就意味着当你用完一个Iterator后,需要及时删除它。
当一次Compaction结束后(会生成新的文件,合并前的文件需要删除),Leveldb会创建一个新的版本作为当前版本,原先的当前版本就会变为历史版本。
VersionSet 是所有Version的集合,管理着所有存活的Version。
VersionEdit 表示Version之间的变化,相当于delta 增量,表示有增加了多少文件,删除了文件。下图表示他们之间的关系。
Version0 +VersionEdit–>Version1
VersionEdit会保存到MANIFEST文件中,当做数据恢复时就会从MANIFEST文件中读出来重建数据。
leveldb的这种版本的控制,让我想到了双buffer切换,双buffer切换来自于图形学中,用于解决屏幕绘制时的闪屏问题,在服务器编程中也有用处。
比如我们的服务器上有一个字典库,每天我们需要更新这个字典库,我们可以新开一个buffer,将新的字典库加载到这个新buffer中,等到加载完毕,将字典的指针指向新的字典库。
leveldb的version管理和双buffer切换类似,但是如果原version被某个iterator引用,那么这个version会一直保持,直到没有被任何一个iterator引用,此时就可以删除这个version。
注:本文参考了这篇文章:http://www.samecity.com/blog/Index.asp?SortID=12
参考资料:1.维基百科
2.google code
1 | struct bufferevent { |
可以看出 struct bufferevent 内置了两个 event(读/写)和对应的缓冲区。当有数据被读入(input)的时候,readcb 被调用,当 output 被输出完成的时候,writecb 被调用,当网络 I/O 出现错误,如链接中断,超时或其他错误时,errorcb 被调用。
使用 bufferevent 的过程:
1. 设置sock为非阻塞的
1 | // eg: evutil_make_socket_nonblocking(fd); |
2. 使用bufferevent_socket_new创建一个structbufferevent *bev,关联该sockfd,托管给event_base
函数原型为:
1 | struct bufferevent * bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, int options) |
3. 设置读写对应的回调函数
函数原型为:
1 | void bufferevent_setcb(struct bufferevent *bufev, |
4. 启用读写事件,其实是调用了event_add将相应读写事件加入事件监听队列poll。正如文档所说,如果相应事件不置为true,bufferevent是不会读写数据的
函数原型:
1 | int bufferevent_enable(struct bufferevent *bufev, short event) |
5. 进入bufferevent_setcb回调函数:
在readcb里面从input中读取数据,处理完毕后填充到output中; writecb对于服务端程序,只需要readcb就可以了,可以置为NULL; errorcb 用于处理一些错误信息。
针对这些使用过程进入源码进行分析:
1. bufferevent_socket_new
2. bufferevent_setcb
该函数的作用主要是赋值,把该函数后面的参数,赋值给第一个参数 struct bufferevent *bufev
定义的变量
3. bufferevent_enable
调用event_add将读写事件加入到事件监听队列中。
对bufferevent常用的几个函数进行分析:
1 | char *evbuffer_readln(struct evbuffer*buffer, size_t *n_read_out,enum |
1 | int evbuffer_add(struct evbuffer *buf,const void *data, size_t datlen); |
1 | int evbuffer_remove(struct evbuffer*buf, void *data, size_t datlen); |
1 | size_t evbuffer_get_length(const structevbuffer *buf); |
暂时先分析到这里,下面是代码,客户端发送消息:
1 | HTTP/1.0, Client 0 send Message: |
服务端一条消息收完成后,会回复:
1 | Response ok! Hello Client! |
服务端从bufferevent中取出消息是按行取的。代码可能有不完善的地方,由于才疏学浅,研究时间短(3天),希望高手提出宝贵意见。
1 |
|
怎么样才算得上熟悉多线程编程?
C++多线程模型与锁
这个大致是一些公司对多线程部分的要求,如果应聘者声称熟悉这个部分。上面所有点都是本人面试被问到的,基本上能看完上面这些,可以做到不用很心虚在简历上写自己熟悉多线程而不会被揭穿。
]]>其实在 Linux 下设计并发网络程序,向来不缺少方法,比如典型的 Apache 模型(Process Per Connection,简称 PPC),TPC(Thread Per Connection)模型,以及 select 模型和 poll 模型,那为何还要再引入 Epoll 这个东东呢?那还是有得说说的…
如果不摆出来其他模型的缺点,怎么能对比出 Epoll 的优点呢。
这两种模型思想类似,就是让每一个到来的连接一边自己做事去,别再来烦我。只是 PPC 是为它开了一个进程,而 TPC 开了一个线程。可是别烦我是有代价的,它要时间和空间啊,连接多了之后,那么多的进程 / 线程切换,这开销就上来了;因此这类模型能接受的最大连接数都不会高,一般在几百个左右。
最大并发数限制,因为一个进程所打开的 FD(文件描述符)是有限制的,由 FD_SETSIZE 设置,默认值是 1024/2048,因此 Select 模型的最大并发数就被相应限制了。自己改改这个FD_SETSIZE?想法虽好,可是先看看下面吧…
效率问题,select 每次调用都会线性扫描全部的 FD 集合,这样效率就会呈现线性下降,把FD_SETSIZE 改大的后果就是,大家都慢慢来,什么?都超时了??!!
内核 / 用户空间 内存拷贝问题,如何让内核把 FD 消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法。
基本上效率和 select 是相同的,select 缺点的 2 和 3 它都没有改掉。
把其他模型逐个批判了一下,再来看看 Epoll 的改进之处吧,其实把 select 的缺点反过来那就是 Epoll 的优点了。
Epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大,具体数目可以 cat /proc/sys/fs/file-max
察看。
效率提升,Epoll 最大的优点就在于它只管你 “活跃” 的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll 的效率就会远远高于 select 和 poll。
内存拷贝,Epoll 在这点上使用了 “共享内存”,这个内存拷贝也省略了。
Epoll 的高效和其数据结构的设计是密不可分的,这个下面就会提到。
首先回忆一下 select 模型,当有 I/O 事件到来时,select 通知应用程序有事件到了快去处理,而应用程序必须轮询所有的 FD 集合,测试每个 FD 是否有事件发生,并处理事件;
1 | int res = select(maxfd+1, &readfds, NULL, NULL, 120); |
Epoll 不仅会告诉应用程序有 I/0 事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个 FD 集合。
1 | intres = epoll_wait(epfd, events, 20, 120); |
前面提到 Epoll 速度快和其数据结构密不可分,其关键数据结构就是:
1 | struct epoll_event { |
结构体 epoll_event
被用于注册所感兴趣的事件和回传所发生待处理的事件.
其中 epoll_data
联合体用来保存触发事件的某个文件描述符相关的数据.
例如一个 client 连接到服务器,服务器通过调用 accept 函数可以得到于这个 client 对应的 socket 文件描述符,可以把这文件描述符赋给 epoll_data 的 fd 字段以便后面的读写操作在这个文件描述符上进行。epoll_event 结构体的 events 字段是表示感兴趣的事件和被触发的事件可能的取值为:
ET 和 LT 模式
LT(level triggered) 是缺省的工作方式,并且同时支持 block 和 no-block socket. 在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的 select/poll 都是这种模型的代表。
ET (edge-triggered) 是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个 EWOULDBLOCK 错误)。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在 TCP 协议中,ET 模式的加速效用仍需要更多的 benchmark 确认。
ET 和 LT 的区别在于 LT 事件不会丢弃,而是只要读 buffer 里面有数据可以让用户读,则不断的通知你。而 ET 则只在事件发生之时通知。可以简单理解为 LT 是水平触发,而 ET 则为边缘触发。
ET 模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用 ET 模式,需要一直 read/write
直到出错为止, 很多人反映为什么采用 ET 模式只接收了一部分数据就再也得不到通知了, 大多因为这样; 而 LT 模式是只要有数据没有处理就会一直通知下去的.
既然 Epoll 相比 select 这么好,那么用起来如何呢?会不会很繁琐啊…先看看下面的三个函数吧,就知道 Epoll 的易用了。
1 | int epoll_create(int size); |
生成一个 Epoll 专用的文件描述符,其实是申请一个内核空间,用来存放你想关注的 socket fd 上是否发生以及发生了什么事件。size 就是你在这个 Epoll fd 上能关注的最大 socket fd 数,大小自定,只要内存足够。
1 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
第三个参数是需要监听的 fd,第四个参数是告诉内核需要监听什么事
1 | int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout); |
等待 I/O 事件的发生;参数说明:
epoll_create()
生成的 Epoll 专用的文件描述符;首先对服务端和客户端做下说明:
我想实现的是客户端和服务端并发的程序,客户端通过配置并发数,说明有多少个用户去连接服务端。
客户端会发送消息:”Client: i send message Hello Server!”,其中 i
表示哪一个客户端;收到消息:”Recv Server Msg Content:%s\n”。
例如:
1 | 发送:Client: 1 send message "Hello Server!" |
例如:
1 | 发送:Hello, client fd: 6 |
备注:这里在接收到消息后,直接打印出消息,如果需要对消息进行处理(如果消息处理比较占用时间,不能立即返回,可以将该消息放入一个队列中,然后开启一个线程从队列中取消息进行处理,这样的话不会因为消息处理而阻塞 epoll)。libenent 好像对这种有 2 中处理方式,一个就是回调,要求回调函数,不占用太多的时间,基本能立即返回,另一种好像也是一个队列实现的,这个还需要研究。
服务端代码说明:
服务端在绑定监听后,开启了一个线程,用于负责接收客户端连接,加入到 epoll 中,这样只要 accept 到客户端的连接,就将其 add EPOLLIN 到 epoll 中,然后进入循环调用 epoll_wait,监听到读事件,接收数据,并将事件修改为 EPOLLOUT;反之监听到写事件,发送数据,并将事件修改为EPOLLIN。
1 | //cepollserver.h |
1 |
|
说明:测试是两个并发进行测试,每一个客户端都是一个长连接。代码中在连接服务器(ConnectToServer)时将用户 ID 和 socketid 关联起来。用户 ID 和 socketid 是一一对应的关系。
1 |
|
1 |
|
1 |
|
1 |
|
虽然对于优化C代码有很多有效的指导方针,但是对于彻底地了解编译器和你工作的机器依然无法取代,通常,加快程序的速度也会加大代码量。这些增加的代码也会影响一个程序的复杂度和可读性,这是不可接受的,比如你在一些小型的设备上编程,例如:移动设备、PDA……,这些有着严格的内存限制,于是,在优化的座右铭是:写代码在内存和速度都应该优化。
在我们知道使用的数不可能是负数的时候,应该使用unsigned int取代int,一些处理器处理整数算数运算的时候unsigned int比int快,于是,在一个紧致的循环里面定义一个整型变量,最好这样写代码:
register unsigned int variable_name;
然而,我们不能保证编译器会注意到那个register关键字,也有可能,对某种处理器来说,有没有unsigned是一样的。这两个关键字并不是可以在所有的编译器中应用。
**记住,整形数运算要比浮点数运算快得多,因为处理器可以直接进行整型数运算,浮点数运算需要依赖于外部的浮点数处理器或者浮点数数学库。**我们处理小数的时候要精确点些(比如我们在做一个简单的统计程序时),要限制结果不能超过100,要尽可能晚的把它转化成浮点数。
还有一个整形提升的问题,比如下面这个例子:
size_t n = 10;int i ;for(i = -1; i < n; ++i){printf("%d\n",i);}这段代码实际上什么也不会输出,因为size_t是unsigned int类型,i会自动转换成unsigned int就变成了一个很大的正数,所以和n比较自然什么都不会输出。
在算术运算中,char和short会自动转换成int,转换的原则就是如果int类型能过包括操作数类型的所有范围,则操作数(比如unsigned short)转换成int,否则转换成unsigned int,int和long类型运算以此类推,**总是向着精度更高、位更长的类型转换。**
在标准的处理器中,根据分子和分母的不同,一个32位的除法需要20-140个时钟周期来执行完成,等于一个固定的时间加上每个位被除的时间。
Time (分子/ 分母) = C0 + C1* log2 (分子/分母)
= C0 + C1 * (log2 (分子) – log2 (分母)).
现在的ARM处理器需要消耗20+4.3N个时钟周期,这是一个非常费时的操作,要尽可能的避免。在有些情况下,除法表达式可以用乘法表达是来重写。比方说,(a/b)>c可以写成a>(c*b),条件是我们已经知道b为非负数而且b*c不会超过整型数的取值范围。如果我们能够确定其中的一个操作数为unsigned,那么使用无符号除法将会更好,因为它要比有符号除法快得多。
在一些情况下,除法运算和取余运算都需要用到,在这种情况下,编译器会将除法运算和取余运算合并,因为除法运算总是同时返回商和余数。如果两个运算都要用到,我们可以将他们写到一起。
typedef unsigned int uint;uint div32u (uint a) { return a / 32;}int div32s (int a) { return a / 32;}
这两种除法都会避免调用除法函数(进行移位操作),另外,无符号的除法要比有符号的除法使用更少的指令。有符号的除法要耗费更多的时间,因为这种除法是使最终结果趋向于零的,而移位则是趋向于负无穷。
我们一般使用取余运算进行取模,不过,有时候使用 if 语句来重写也是可行的。考虑下面的两个例子:
uint modulo_func1 (uint count){ return (++count % 60);}uint modulo_func2 (uint count){ if (++count >= 60) count = 0; return (count);}
第二个例子要比第一个更可取,因为由它产生的代码会更快,注意:这只是在count取值范围在0 – 59之间的时候才行。
但是我们可以使用如下的代码(笔者补充)实现等价的功能:
uint modulo_func3 (uint count){ if (++count >= 60) count %= 60; return (count);}
假设你要依据某个变量的值,设置另一个变量的取值为特定的字符,你可能会这样做:
switch(queue) { case 0 : letter = 'W'; break; case 1 : letter = 'S'; break; case 2 : letter = 'U'; break;}
或者这样:
if(queue == 0) letter = 'W';else if ( queue == 1 ) letter = 'S';else letter = 'U';
有一个简洁且快速的方式是简单的将变量的取值做成一个字符串索引,例如:
static char *classes = "WSU";letter = classes[queue];
全局变量不会被分配在寄存器上,修改全局变量需要通过指针或者调用函数的方式间接进行。所以编译器不会将全局变量存储在寄存器中,那样会带来额外的、不必要的负担和存储空间。所以在比较关键的循环中,我们要不使用全局变量。
举个例子:
int f(void);int g(void);int errs;void test1(void){ errs += f(); errs += g();}void test2(void){ int localerrs = errs; localerrs += f(); localerrs += g(); errs = localerrs;}
可以看到test1()中每次加法都需要读取和存储全局变量errs,而在test2()中,localerrs分配在寄存器上,只需要一条指令。
考虑下面的例子:
void func1( int *data ){ int i; for(i = 0; i < 10; i++) anyfunc(*data, i);}
即使*data从来没有变化,编译器却不知道anyfunc()没有修改它,于是程序每次用到它的时候,都要把它从内存中读出来,可能它只是某些变量的别名,这些变量在程序的其他部分被修改。如果能够确定它不会被改变,我们可以这样写:
void func1( int *data ){int i;int localdata;localdata = *data;for(i=0; i<10; i++)anyfunc(localdata, i);}
这样会给编译器优化工作更多的选择余地。
寄存器的数量在每个处理器当中都是固定的,所以在程序的某个特定的位置,可以保存在寄存器中的变量的数量是有限制的。有些编译器支持“生命周期分割”(live-range splitting),也就是说在函数的不同部分,变量可以被分配到不同的寄存器或者内存中。变量的生存范围被定义成:起点是对该变量的一次空间分配,终点是在下次空间分配之前的最后一次使用之间。在这个范围内,变量的值是合法的,是活的。在生存范围之外,变量不再被使用,是死的,它的寄存器可以供其他变量使用,这样,编译器就可以安排更多的变量到寄存器当中。
可分配到寄存器的变量需要的寄存器数量等于经过生命范围重叠的变量的数目,如果这个数目超过可用的寄存器的数量,有些变量就必须被暂时的存储到内存中。这种处理叫做“泄漏(spilling)”。
编译器优先释放最不频繁使用的变量,将释放的代价降到最低。可以通过以下方式避免变量的“释放”:
C编译器支持基本的变量类型:char、short、int、long(signed、unsigned)、float、double。为变量定义最恰当的类型,非常重要,因为这样可以减少代码和数据的长度,可以非常显著的提高效率。
如果可能,局部变量要避免使用char和short。对于char和short类型,编译器在每次分配空间以后,都要将这种局部变量的尺寸减少到8位或16位。这对于符号变量来说称为符号扩展,对无符号变量称为无符号扩展。这种操作是通过将寄存器左移24或16位,然后再有符号(或无符号的)右移同样的位数来实现的,需要两条指令(无符号字节变量的无符号扩展需要一条指令)。
这些移位操作可以通过使用int和unsigned int的局部变量来避免。这对于那些首先将数据调到局部变量然后利用局部变量进行运算的情况尤其重要。即使数据以8位或16位的形式输入或输出,把他们当作32位来处理仍是有意义的。
我们来考虑下面的三个例子函数:
int wordinc (int a){ return a + 1;}short shortinc (short a){ return a + 1;}char charinc (char a){ return a + 1;}
他们的运算结果是相同的,但是第一个代码片断要比其他片断运行的要快。
如果可能,我们应该使用结构体的引用作为参数,也就是结构体的指针,否则,整个结构体就会被压入堆栈,然后传递,这会降低速度。程序适用值传递可能需要几K字节,而一个简单的指针也可以达到同样的目的,只需要几个字节就可以了。
如果在函数内部不会改变结构体的内容,那么就应该将参数声明为const型的指针。举个例子:
void print_data_of_a_structure (const Thestruct *data_pointer){ ...printf contents of the structure...}
这个例子代码告知编译器在函数内部不会改变外部结构体的内容,访问他们的时候,不需要重读。还可以确保编译器捕捉任何修改这个只读结构体的代码,给结构体以额外的保护。
指针链经常被用来访问结构体的信息,比如,下面的这段常见的代码:
typedef struct { int x, y, z; } Point3;typedef struct { Point3 *pos, *direction; } Object;void InitPos1(Object *p){ p->pos->x = 0; p->pos->y = 0; p->pos->z = 0;}
代码中,处理器在每次赋值操作的时候都要重新装载p->pos,因为编译器不知道p->pos->x不是p->pos的别名。更好的办法是将p->pos缓存成一个局部变量,如下:
void InitPos2(Object *p){ Point3 *pos = p->pos; pos->x = 0; pos->y = 0; pos->z = 0;}
另一个可能的方法是将Point3结构体包含在Object结构体中,完全避免指针的使用。
条件执行主要用在if语句中,同时也会用到由关系运算(<,==,>等)或bool运算(&&, !等)组成的复杂的表达式。尽可能的保持if和else语句的简单是有好处的,这样才能很好的条件化。关系表达式应该被分成包含相似条件的若干块。
下面的例子演示了编译器如何使用条件执行:
int g(int a, int b, int c, int d){ if(a > 0 && b > 0 && c < 0 && d < 0) //分组化的条件被捆绑在一起 return a + b + c + d; return -1;}
条件被分组,便以其能够条件化他们。
有一种常见的boolean表达式被用来检查是否一个变量取值在某个特定的范围内,比方说,检查一个点是否在一个窗口内。
bool PointInRectangelArea (Point p, Rectangle *r){ return (p.x >= r->xmin && p.x < r->xmax && p.y >= r->ymin && p.y < r->ymax);}
这里还有一个更快的方法:把(x >= min && x < max) 转换成 (unsigned)(x-min) < (max-min). 尤其是min为0时,更为有效。下面是优化后的代码:
bool PointInRectangelArea (Point p, Rectangle *r){ return ((unsigned) (p.x - r->xmin) < r->xmax && (unsigned) (p.y - r->ymin) < r->ymax);}
在比较(CMP)指令后,相应的处理器标志位就会被设置。这些标志位也可以被其他的指令设置,诸如MOV, ADD, AND, MUL, 也就是基本的数学和逻辑运算指令(数据处理指令)。假如一条数据处理指令要设置这些标志位,那么N和Z标志位的设置方法跟把数字和零比较的设置方法是一样的。N标志位表示结果是不是负数,Z标志位表示结果是不是零。
在C语言中,处理器中的N和Z标志位对应的有符号数的关系运算符是x < 0, x >= 0, x == 0, x != 0,无符号数对应的是x == 0, x != 0 (or x > 0)。
C语言中,每用到一个关系运算符,编译器就会产生一个比较指令。如果关系运算符是上面的其中一个,在数据处理指令紧跟比较指令的情况下,编译器就会将比较指令优化掉。比如:
int aFunction(int x, int y){ if (x + y < 0) return 1; else return 0;}
这样做,会在关键循环中节省比较指令,使代码长度减少,效率增加。C语言中没有借位(carry)标志位和溢出(overflow)标志位的概念,所以如果不使用内嵌汇编语言,要访问C和V标志位是不可能的。尽管如此,编译器支持借位标志位(无符号数溢出),比方说:
int sum(int x, int y){ int res; res = x + y; if ((unsigned) res < (unsigned) x) // carry set? // res++; return res;}
在类似与这样的 if(a>10 && b=4) 语句中, 确保AND表达式的第一部分最有可能为false, 结果第二部分极有可能不被执行.
用switch() 代替if…else…,在条件选择比较多的情况下,可以用if…else…else…,像这样:
if( val == 1) dostuff1();else if (val == 2) dostuff2();else if (val == 3) dostuff3();
使用switch可以更快:
switch( val ){ case 1: dostuff1(); break; case 2: dostuff2(); break; case 3: dostuff3(); break;}
在if语句中,即使是最后一个条件成立,也要先判断所有前面的条件是否成立。Switch语句能够去除这些额外的工作。如果你不得不使用if…else,那就把最可能的成立的条件放在前面。
把判断条件做成二进制的风格,比如,不要使用下面的列表:
if(a == 1) { } else if(a == 2) { } else if(a == 3) { } else if(a == 4) { } else if(a == 5) { } else if(a == 6) { } else if(a == 7) { } else if(a == 8) { }}
而采用:
if(a <= 4) { if(a == 1) { } else if(a == 2) { } else if(a == 3) { } else if(a == 4) { } } else { if(a == 5) { } else if(a == 6) { } else if(a == 7) { } else if(a == 8) { } }
甚至:
if(a <= 4) { if(a <= 2) { if(a == 1) { /* a is 1 */ } else { /* a must be 2 */ } } else { if(a == 3) { /* a is 3 */ } else { /* a must be 4 */ } } } else { if(a <= 6) { if(a == 5) { /* a is 5 */ } else { /* a must be 6 */ } } else { if(a == 7) { /* a is 7 */ } else { /* a must be 8 */ } } }
慢速、低效:
c = getch();switch(c){ case 'A': { do something; break; } case 'H': { do something; break; } case 'Z': { do something; break; }}
快速、高效:
c = getch();switch(c) { case 0: { do something; break; } case 1: { do something; break; } case 2: { do something; break; }}
以上是两个case语句之间的比较
switch语句通常用于以下情况:
如果case表示是密集的,在使用switch语句的前两种情况中,可以使用效率更高的查找表。比如下面的两个实现汇编代码转换成字符串的例程:
char * Condition_String1(int condition) { switch(condition) { case 0: return "EQ"; case 1: return "NE"; case 2: return "CS"; case 3: return "CC"; case 4: return "MI"; case 5: return "PL"; case 6: return "VS"; case 7: return "VC"; case 8: return "HI"; case 9: return "LS"; case 10: return "GE"; case 11: return "LT"; case 12: return "GT"; case 13: return "LE"; case 14: return ""; default: return 0; }}char * Condition_String2(int condition) { if((unsigned) condition >= 15) return 0; return "EQNECSCCMIPLVSVCHILSGELTGTLE" + 3 * condition;}
第一个例程需要240个字节,第二个只需要72个。
如果不加留意地编写循环终止条件,就可能会给程序带来明显的负担。我们应该尽量使用“倒数到零”的循环,使用简单的循环终止条件。循环终止条件相对简单,程序在执行的时候也会消耗相对少的时间。拿下面两个计算n!的例子来说,第一个例子使用递增循环,第二个使用递减循环。
int fact1_func (int n){ int i, fact = 1; for (i = 1; i <= n; i++) fact *= i; return (fact);}int fact2_func(int n)
{ int i, fact = 1;
for (i = n; i != 0; i–)
fact *= i;
return (fact);
}
结果是,第二个例子要比第一个快得多。
这是一个简单而有效的概念,通常情况下,我们习惯把for循环写成这样:
for( i = 0; i < 10; i++){ ... }
i 值依次为:0,1,2,3,4,5,6,7,8,9
在不在乎循环计数器顺序的情况下,我们可以这样:
for( i = 10; i--; ) { ... }
i 值依次为: 9,8,7,6,5,4,3,2,1,0,而且循环要更快
这种方法是可行的,因为它是用更快的i–作为测试条件的,也就是说“i是否为非零数,如果是减一,然后继续”。相对于原先的代码,处理器不得不“把i减去10,结果是否为非零数,如果是,增加i,然后继续”,在紧密循环(tight loop)中,这会产生显著的区别。
这种语法看起来有一点陌生,却完全合法。循环中的第三条语句是可选的(无限循环可以写成这样for(;;)),下面的写法也可以取得同样的效果:
for(i = 10; i; i--){}
或者:
for(i = 10; i != 0; i--){}
我们唯一要小心的地方是要记住循环需要停止在0(如果循环是从50-80,这样做就不行了),而且循环的计数器为倒计数方式。
另外,我们还可以把计数器分配到寄存器上,可以产生更为有效的代码。这种将循环计数器初始化成循环次数,然后递减到零的方法,同样适用于while和do语句。
在可以使用一个循环的场合,决不要使用两个。但是如果你要在循环中进行大量的工作,超过处理器的指令缓冲区,在这种情况下,使用两个分开的循环可能会更快,因为有可能这两个循环都被完整的保存在指令缓冲区里了。
//原先的代码for(i = 0; i < 100; i++){ stuff();}for(i = 0; i < 100; i++){ morestuff();} //更好的做法for(i = 0; i < 100; i++){ stuff(); morestuff();}
调用函数的时候,在性能上就会付出一定的代价。不光要改变程序指针,还要将那些正在使用的变量压入堆栈,分配新的变量空间。为了提高程序的效率,在程序的函数结构上,有很多工作可以做。保证程序的可读性的同时,还要尽量控制程序的大小。
如果一个函数在一个循环中被频繁调用,就可以考虑将这个循环放在函数的里面,这样可以免去重复调用函数的负担,比如:
for(i = 0 ; i < 100 ; i++) { func(t,i); }void func(int w, d) { lots of stuff. }
可以写成:
func(t);void func(w) { for(i = 0; i < 100; i++) { //lots of stuff. } }
为了提高效率,可以将小的循环解开,不过这样会增加代码的尺寸。循环被拆开后,会降低循环计数器更新的次数,减少所执行的循环的分支数目。如果循环只重复几次,那它完全可以被拆解开,这样,由循环所带来的额外开销就会消失。
比如:
for(i = 0; i < 3; i++){ something(i);}//更高效的方式:something(0);something(1);something(2);
因为在每次的循环中,i 的值都会增加,然后检查是否有效。编译器经常会把这种简单的循环解开,前提是这些循环的次数是固定的。对于这样的循环:
for(i = 0; i < limit; i++) { ... }
就不可能被拆解,因为我们不知道它循环的次数到底是多少。不过,将这种类型的循环拆解开并不是不可能的。
与简单循环相比,下面的代码的长度要长很多,然而具有高得多的效率。选择8作为分块大小,只是用来演示,任何合适的长度都是可行的。例子中,循环的成立条件每八次才被检验一次,而不是每次都要检验。如果需要处理的数组的大小是确定的,我们就可以使用数组的大小作为分块的大小(或者是能够整除数组长度的数值)。不过,分块的大小跟系统的缓存大小有关。
#include<stdio.H> #define BLOCKSIZE (8)int main(void)
{ int i = 0;
int limit = 33; /* could be anything */
int blocklimit;/* The limit may not be divisible by BLOCKSIZE, go as near as we can first, then tidy up. */ blocklimit = (limit / BLOCKSIZE) * BLOCKSIZE;/* unroll the loop in blocks of 8 */ while(i < blocklimit) { printf("process(%d)\n", i); printf("process(%d)\n", i+1); printf("process(%d)\n", i+2); printf("process(%d)\n", i+3); printf("process(%d)\n", i+4); printf("process(%d)\n", i+5); printf("process(%d)\n", i+6); printf("process(%d)\n", i+7); /* update the counter */ i += 8; } /* * There may be some left to do. * This could be done as a simple for() loop, * but a switch is faster (and more interesting) */ if( i < limit ) { /* Jump into the case at the place that will allow * us to finish off the appropriate number of items. */ switch( limit - i ) { case 7 : printf("process(%d)\n", i); i++; case 6 : printf("process(%d)\n", i); i++; case 5 : printf("process(%d)\n", i); i++; case 4 : printf("process(%d)\n", i); i++; case 3 : printf("process(%d)\n", i); i++; case 2 : printf("process(%d)\n", i); i++; case 1 : printf("process(%d)\n", i); }} return 0;
}
例1:测试单个的最低位,计数,然后移位。
//example1int countbit1(uint n){ int bits = 0; while (n != 0) { if(n & 1) bits++; n >>= 1; } return bits;}
例2:先除4,然后计算被4处的每个部分。循环拆解经常会给程序优化带来新的机会。
//example - 2int countbit2(uint n){ int bits = 0; while (n != 0) { if (n & 1) bits++; if (n & 2) bits++; if (n & 4) bits++; if (n & 8) bits++; n >>= 4; } return bits;}
通常没有必要遍历整个循环。举例来说,在数组中搜索一个特定的值,我们可以在找到我们需要值之后立刻退出循环。下面的例子在10000个数字中搜索-99。
found = FALSE; for(i=0;i<10000;i++) { if(list[i] == -99) { found = TRUE; } } if(found) printf("Yes, there is a -99. Hooray!\n");
这样做是可行的,但是不管这个被搜索到的项目出现在什么位置,都会搜索整个数组。跟好的方法是,再找到我们需要的数字以后,立刻退出循环。
found = FALSE; for(i = 0; i < 10000; i++) { if( list[i] == -99 ) { found = TRUE; break; } } if( found ) printf("Yes, there is a -99. Hooray!\n");
如果数字出现在位置23上,循环就会终止,忽略剩下的9977个。
保持函数短小精悍,是对的。这可以使编译器能够跟高效地进行其他的优化,比如寄存器分配。
对处理器而言,调用函数的开销是很小的,通常,在被调用函数所进行的工作中,所占的比例也很小。能够使用寄存器传递的函数参数个数是有限制的。这些参数可以是整型兼容的(char,short,int以及float都占用一个字),或者是4个字以内的结构体(包括2个字的double和long long)。假如参数的限制是4,那么第5个及后面的字都会被保存到堆栈中。这会增加在调用函数是存储这些参数的,以及在被调用函数中恢复这些参数的代价。
int f1(int a, int b, int c, int d) { return a + b + c + d;}int g1(void) { return f1(1, 2, 3, 4);}int f2(int a, int b, int c, int d, int e, int f) { return a + b + c + d + e + f;}ing g2(void) { return f2(1, 2, 3, 4, 5, 6);}
g2函数中,第5、6个参数被保存在堆栈中,在f2中被恢复,每个参数带来2次内存访问。
为了将传递参数给函数的代价降至最低,我们可以:
尽可能确保函数的形参不多于四个,甚至更少,这样就不会使用堆栈来传递参数。
如果一个函数形参多于四个,那就确保在这个函数能够做大量的工作,这样就可以抵消由传递堆栈参数所付出的代价。
用指向结构体的指针作形参,而不是结构体本身。
把相关的参数放到一个结构里里面,然后把它的指针传给函数,可以减少参数的个数,增加程序的可读性。
将long类型的参数的个数降到最小,因为它使用两个参数的空间。对于double也同样适用。
避免出现参数的一部分使用寄存器传输,另一部分使用堆栈传输的情况。这种情况下参数将被全部压到堆栈里。
避免出现函数的参数个数不定的情况。这种情况下,所有参数都使用堆栈。
如果一个函数不再调用其他函数,这样的函数被称为叶子函数。在许多应用程序中,大约一半的函数调用是对叶子函数的调用。叶子函数在所有平台上都可以得到非常高效的编译,因为他们不需要进行参数的保存和恢复。在入口压栈和在出口退栈的代价,跟一个足够复杂的需要4个或者5个参数的叶子函数所完成的工作相比,是非常小的。如果可能的话,我们就要尽量安排经常被调用的函数成为叶子函数。函数被调用的次数可以通过模型工具(profiling facility)来确定。这里有几种方法可以确保函数被编译成叶子函数:
对于所有调试选项,内嵌函数是被禁止的。使用inline关键字修饰函数后,跟普通的函数调用不同,代码中对该函数的调用将会被函数体本身代替。这会使代码更快,另一方面它会影响代码的长度,尤其是内嵌函数比较大而且经常被调用的情况下。
__inline int square(int x) { return x * x;}double length(int x, int y){ return sqrt(square(x) + square(y));}
使用内嵌函数有几个优点:
因为函数被直接代替,没有任何额外的开销,比如存储和恢复寄存器。
参数传递的开销通常会更低,因为它不需要复制变量。如果其中一些参数是常量,编译器还可以作进一步的优化。
内嵌函数的缺点是如果函数在许多地方被调用,将会增加代码的长度。长度差别的大小非常依赖于内嵌函数的大小和调用的次数。
仅将少数关键函数设置成内嵌函数是明智的。如果设置得当,内嵌函数可以减少代码的长度,一次函数调用需要一定数量的指令,但是,使用优化过的内嵌函数可以编译成更少的指令。
有些函数可以近似成查找表,这样可以显著的提高效率。查找表的精度一般比计算公式的精度低,不过在大多数程序中,这种精度就足够了。
许多信号处理软件(比如MODEM调制软件)会大量的使用sin和cos函数,这些函数会带来大量的数学运算。对于实时系统来说,精度不是很重要,sin/cos查找表显得更加实用。使用查找表的时候,尽量将相近的运算合并成一个查找表,这样要比使用多个查找表要更快和使用更少的空间。
尽管浮点运算对于任何处理器来讲都是很费时间的,有的时候,我们还是不得不用到浮点运算,比方说实现信号处理。尽管如此,编写浮点运算代码的时候,我们要牢记:
除法要比加法或者乘法慢两倍,我们可以把被一个常数除的运算写成被这个数的倒数乘(比如,x=x/3.0写成x=x*(1.0/3.0))。倒数的计算在编译阶段就被完成。
Float型变量消耗更少的内存和寄存器,而且因为它的低精度所以具有更高的效率。在精度足够的情况下,就要使用float。
先验函数(比如sin,cos,log)是通过使用一系列的乘法和加法实现的,所以这些运算会比普通的乘法慢10倍以上。
编译器在整型跟浮点型混合的运算中不会进行太多的优化。比如3 * (x / 3) 不会被优化成x,因为浮点运算通常会导致精度的降低,甚至表达式的顺序都是重要的: (a + b) + c 不等于 a + (b + c)。因此,进行手动的优化是有好处的。
不过,在特定的场合下,浮点运算的效率达不到指定的水平,这种情况下,最好的办法可能是放弃浮点运算,转而使用定点运算。当变量的变化范围足够的小,定点运算要比浮点运算精度更高、速度更快。
http://www.xs4all.nl/~ekonijn/loopy.html
http://www.public.asu.edu/~sshetty/Optimizing_Code_Manual.doc
http://www.abarnett.demon.co.uk/tutorial.html
本文翻译自: codeproject,感谢codingwu的整理,转载请注明出处。
]]>大部分 C 程序员都以为基本的整形操作都是安全的其实不然,看下面这个例子,
你觉得输出结果是什么:
1 | int main(int argc, char** argv) { |
当一个变量转换成无符号整形时,i的值不再是-1,而是 size_t的最大值,因为sizeof操作返回的是一个 size_t类型的无符号数。
在C99/C11标准里写道:
“If the operand that has unsigned integer type has rank greater or
equal to the rank of the type of the other operand, then the operand
with signed integer type is converted to the type of the operand with
unsigned integer type.”
在 C 标准里面 size_t 至少是一个 16 位的无符号整数, 对于给定的架构 size_t 一般对应 long, 所以sizeof(int)和 size_t 至少相等, 这就带来了可移植性的问题, C 标准没有定义 short, int, long, longlong 的大小, 只是说明了他们的最小长度, 对于 x86_64 架构, long 在Linux下是 64 位, 而在 64 位 Windows 下是 32 位。一般的方法是采用固定长度的类型比如定义在 C99 头文件stdint.h中的uint16_t, int32_t, uint_least16_t, uint_fast16_t 等。
如果 int 可以表示原始类型的所有值,那么这个操作数会转换成 int, 否则他会转换成 unsigned int。下面这个函数在 32 位平台返回 65536, 但是在 16 位系统返回 0。
1 | uint32_t sum() |
对于char 类型到底是 signed 还是 unsigned 取决于硬件架构和操作系统,通常
由特定平台的 ABI(Application Binary Interface) 指定,如果是 signed char,下面的代码输出-128 和-127,否则输出 128,129(x86 架构)。
char c = 128;char d = 129;printf("%d,%d\n",c,d);
malloc 函数分配制定字节大小的内存,对象未被初始化,如果 size 是 0 取
决与系统实现。malloc(0) 返回一个空指针或者 unique pointer, 如果 size 是表达式的运算结果, 确保没有整形溢出。
“If the size of the space requested is 0, the behavior is
implementation- defined: the value returned shall be either a null
pointer or a unique pointer.”
1 | size_t computed_size; |
malloc 不会给分配的内存初始化,如果要对新分配的内存初始化,可以用 calloc 代替 malloc, 一般情况下给序列分配相等大小的元素时, 用 calloc 来代替用表达式计算大小, calloc 会把内存初始化为 0。
realloc 用来对已经分配内存的对象改变大小,如果新的 size 更大,额外的空间
没 有 被 初 始 化 , 如 果 提 供 给 realloc 的 指 针 是 空 指 针 , realloc 就 等 效 于malloc,如果原指针非空而 new size是0,结果依赖于操作系统的具体实现。
“In case of failure realloc shall return NULL and leave provided memory
object intact. Thus it is important not only to check for integer
overflow of size argument, but also to correctly handle object size if
realloc fails.”
1 |
|
使用未初始化的变量
C 语言要求所有变量在使用之前要初始化,使用未初始化的变量会造成为定义的行为,这和 C++ 不同,C++ 保证所有变量在使用之前都得到初始化,Java 尽量保证 变量使用前的得到初始化,如类基本数据成员会被初始化为默认值。
free 错误
对空指针调用 free, 对不是由 malloc family 函数分配的指针调用 free,或者对已经调用 free 的指针再次调用 free。
一开始初始化指针为 NULL 可以减少错误, GCC 和 Clang 编译器有 -Wuninitialized 选项来对未初始化的变量显示警告信息, 另外不要将同一个指针用于静态变量和动态变量。
1 | char *ptr = NULL; |
对空指针解引用,数组越界访问
对 NULL 指针或者 free’d 内存解引用,数组越界访问,是很明显的错误,为了消除这种错误,一般的做法就是增加数组越界检查的功能,比如 Java 里的 array 就有下标检查的功能,但是这样会带来严重的性能代价,我们要修改 ABI(application binary interface),让每个指针都跟随着它的范围信息,在数值计算中 cost is terrible。
违反类型规则
把 int*
指针 cast 成 float*
,然后对它解引用,在 C 里面会引发 undefined behavior,C 规定这种类型的转换需要使用 memset,C++ 里面有个 reinterpret_cast 函数用于无关类型之间的转换,reinterpret_cast <new_type> (expression)
内存泄漏发生在程序不再使用的动态内存没有得到释放,这需要我们掌握动态分配对象的作用域,尤其是什么时候该调用 free 来释放内存,常用的几种方法如下:
在程序启动的时候分配需要的 heap memory,程序退出时把释放的任务交给操作系统,这种方法一般适用于程序运行后马上退出的那种。
使用变长数组(VLA)
如果你需要一块变长大小的空间并且作用域在函数中,变长数组可以帮到你,但是也有一个限制,一个函数中的变长数组内存大小一般不超过几百字节,这个数字 C 标准没有明确的定义,最好是把内存分配到栈上,在栈上允许分配的最大 VLA 内存是 SIZE_MAX,掌握目标平台的栈大小可以有效的防止栈溢出。
使用引用计数
引用计数是一个很好的管理内存的方法,特别是当你不希望自己定义的对象被复制时,每一次赋值把引用计数加 1, 每次失去引用就把引用计数减1,当引用计数等于0时,以为的对象已经不再需要了,我们需要释放对象占用的内存,由于C不提供自动的析构函数,我们必须手动释放内存,看一个例子:
1 |
|
C++ 标准库有个 auto_ptr
智能指针,能够自动释放指针所指对象的内存,C++ boost 库有个boost::shared_ptr
智能指针,内置引用计数,支持拷贝和赋值,看下面这个例子:
“Objects of shared_ptr types have the ability of taking ownership of a pointer and share that ownership: once they take ownership, the group of owners of a pointer become responsible for its deletion when the last one of them releases that ownership.”
1 |
|
1 |
|
垃圾回收机制
引用计数采用的方法是当内存不再需要时得到手动释放,垃圾回收发生在内存分配失败或者内存到达一定的水位(watermarks),实现垃圾回收最简单的一个算法是 MARK AND SWEEP 算法,该算法的思路是遍历所有动态分配对象的内存,标记那些还能继续使用的,回收那些没有被标记的内存。
Java 采用的垃圾回收机制就更复杂了,思路也是回收那些不再使用的内存,JAVA 的垃圾回收和C++ 的析构函数又不一样,C++ 保证对象在使用之前得到初始化,对象超出作用域之后内存得到释放,而 JAVA 不能保证对象一定被析构。
我们一般的概念里指针和数组名是可互换的,但是在编译器里他们被不同的对待,当我们说一个对象或者表达式具有某种类型的时候我们一般是说这个对象是个左值(lvalue),当对象不是 const 的时候,左值是可以修改的,比如对象是复制操作符的左参数,而数组名是一个 const 左值,指向地一个元素的 const 指针,所以你不能给数组名赋值或者意图改变数组名,如果表达式是数组类型,数组名通常转换成指向地一个元素的指针。
但是也有例外,什么情况下数组名不是一个指针呢?
&
的操作数时,返回一个数组的地址看下面这个例子:
1 | short a[] = {1,2,3}; |
a 是一个 short 类型数组,pa 是一个指向 short 类型的指针,px 呢?
px 是一个指向数组类型的指针,在 a 被赋值给 pa 之前,他的值被转换成一个指向数组第一个元素的指针,下面那个 a 却没有转换,因为遇到的是 &
操作符。
数组下标 a[1]
等价于 (a+1)
, 和 p[1]
一样,也指向 *(p+1)
,但是两者还是有区别的,a 是一个数组,它实际上存储的是第一个元素的地址,所以数组 a 是用来定位第一个元素的,而 pa 不一样,它就是一个指针,不是用来定位的。
再比如:
1 | int a[10]; |
【设计模式】C++设计模式(全26讲)
单例模式(Singleton Pattern,也称为单件模式),使用最广泛的设计模式之一。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
定义一个单例类:
- 私有化它的构造函数,以防止外界创建单例类的对象;
- 使用类的私有静态指针变量指向类的唯一实例;
- 使用一个公有的静态方法获取该实例。
即第一次调用该类实例的时候才产生一个新的该类实例,并在以后仅返回此实例。
需要用锁,来保证其线程安全性:原因:多个线程可能进入判断是否已经存在实例的 if 语句,从而non thread safety.
使用double-check来保证thread safety.但是如果处理大量数据时,该锁才成为严重的性能瓶颈。
1 | class Singleton |
1 | class SingletonInside |
即无论是否调用该类的实例,在程序开始时就会产生一个该类的实例,并在以后仅返回此实例。
由静态初始化实例保证其线程安全性,WHY?因为静态实例初始化在程序开始时进入主函数之前就由主线程以单线程方式完成了初始化,不必担心多线程问题。
故在性能需求较高时,应使用这种模式,避免频繁的锁争夺。
1 | class SingletonStatic |
m_pInstance 指向的空间什么时候释放呢?更严重的问题是,该实例的析构函数什么时候执行?
1 |
|
1 | 输出结果如下: |
类 CGarbo 被定义为 CSingleton 的私有内嵌类,以防该类被在其他地方滥用。
程序运行结束时,系统会调用 CSingleton的 静态成员 Garbo 的析构函数,该析构函数会删除单例的唯一实例。
使用这种方法释放单例对象有以下特征:
在单例类内部定义专有的嵌套类;
在单例类内定义私有的专门用于释放的静态成员;
利用程序在结束时析构全局变量的特性,选择最终的释放时机;
使用单例的代码不需要任何操作,不必关心对象的释放。
在C++中利用反射和简单工厂模式实现业务模块解耦
用一个单独的类来做创造实例的过程,就是工厂。
1 |
|
1 | class OperationFactory { |
面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。
工厂方法模式定义了一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到子类。
1 |
|
把简单工厂模式中的工厂类抽象出一个接口,这个接口只有一个方法,就是创建抽象产品的工厂方法。然后所有的要生产具体类的工厂,就去实现这个接口,这样,一个简单工厂模式的工厂类,就变成了一个工厂抽象接口和多个具体生成对象的工厂。
这样整个工厂和产品体系就没有修改,而只是扩展,符合开放 - 封闭原则。
抽象工厂模式是提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
1 |
|
抽象工厂函数的优缺点
优点:
缺点:增加新的产品时需要改动多处代码。
]]>array是固定大小的顺序容器,它们保存了一个以严格的线性顺序排列的特定数量的元素。
在内部,一个数组除了它所包含的元素(甚至不是它的大小,它是一个模板参数,在编译时是固定的)以外不保存任何数据。存储大小与用语言括号语法([])声明的普通数组一样高效。这个类只是增加了一层成员函数和全局函数,所以数组可以作为标准容器使用。
与其他标准容器不同,数组具有固定的大小,并且不通过分配器管理其元素的分配:它们是封装固定大小数组元素的聚合类型。因此,他们不能动态地扩大或缩小。
零大小的数组是有效的,但是它们不应该被解除引用(成员的前面,后面和数据)。
与标准库中的其他容器不同,交换两个数组容器是一种线性操作,它涉及单独交换范围内的所有元素,这通常是相当低效的操作。另一方面,这允许迭代器在两个容器中的元素保持其原始容器关联。
数组容器的另一个独特特性是它们可以被当作元组对象来处理:array头部重载get函数来访问数组元素,就像它是一个元组,以及专门的tuple_size和tuple_element类型。
1 | template < class T, size_t N > class array; |
返回指向数组容器中第一个元素的迭代器。
1 | iterator begin() noexcept; |
Example
1 |
|
Output
1 | myarray contains: 2 16 77 34 50 |
返回指向数组容器中最后一个元素之后的理论元素的迭代器。
1 | iterator end() noexcept; |
Example
1 |
|
Output
1 | myarray contains: 5 19 77 34 99 |
返回指向数组容器中最后一个元素的反向迭代器。
1 | reverse_iterator rbegin()noexcept; |
Example
1 |
|
Output
1 | myarray contains: 14 80 26 4 |
返回一个反向迭代器,指向数组中第一个元素之前的理论元素(这被认为是它的反向结束)。
1 | reverse_iterator rend() noexcept; |
Example
1 |
|
Output
1 | myarray contains: 14 80 26 4 |
返回指向数组容器中第一个元素的常量迭代器(const_iterator);这个迭代器可以增加和减少,但是不能用来修改它指向的内容。
1 | const_iterator cbegin()const noexcept; |
Example
1 |
|
Output
1 | myarray contains: 2 16 77 34 50 |
返回指向数组容器中最后一个元素之后的理论元素的常量迭代器(const_iterator)。这个迭代器可以增加和减少,但是不能用来修改它指向的内容。
1 | const_iterator cend() const noexcept; |
Example
1 |
|
Output
1 | myarray contains: 2 16 77 34 50 |
返回指向数组容器中最后一个元素的常量反向迭代器(const_reverse_iterator)
1 | const_reverse_iterator crbegin()const noexcept; |
Example
1 |
|
Output
1 | myarray backwards: 60 50 40 30 20 10 |
返回指向数组中第一个元素之前的理论元素的常量反向迭代器(const_reverse_iterator),它被认为是其反向结束。
1 | const_reverse_iterator crend() const noexcept; |
Example
1 |
|
Output
1 | myarray backwards: 60 50 40 30 20 10 |
返回数组容器中元素的数量。
1 | constexpr size_type size()noexcept; |
Example
1 |
|
Possible Output
1 | size of myints: 5 |
返回数组容器可容纳的最大元素数。数组对象的max_size与其size一样,始终等于用于实例化数组模板类的第二个模板参数。
1 | constexpr size_type max_size() noexcept; |
Example
1 |
|
Output
1 | size of myints: 10 |
返回一个布尔值,指示数组容器是否为空,即它的size()是否为0。
1 | constexpr bool empty() noexcept; |
Example
1 |
|
Output:
1 | first is empty |
返回数组中第n个位置的元素的引用。与array::at相似,但array::at会检查数组边界并通过抛出一个out_of_range异常来判断n是否超出范围,而array::operator[]不检查边界。
1 | reference operator[] (size_type n); |
Example
1 |
|
Output
1 | myarray contains: 0 1 2 3 4 5 6 7 8 9 |
返回数组中第n个位置的元素的引用。与array::operator[]相似,但array::at会检查数组边界并通过抛出一个out_of_range异常来判断n是否超出范围,而array::operator[]不检查边界。
1 | reference at ( size_type n ); |
Example
1 |
|
Output
1 | myarray contains: 0 1 2 3 4 5 6 7 8 9 |
返回对数组容器中第一个元素的引用。array::begin返回的是迭代器,array::front返回的是直接引用。
在空容器上调用此函数会导致未定义的行为。
1 | reference front(); |
Example
1 |
|
Output
1 | front is: 2 |
返回对数组容器中最后一个元素的引用。array::end返回的是迭代器,array::back返回的是直接引用。
在空容器上调用此函数会导致未定义的行为。
1 | reference back(); |
Example
1 |
|
Output
1 | front is: 5 |
返回指向数组对象中第一个元素的指针。
由于数组中的元素存储在连续的存储位置,所以检索到的指针可以偏移以访问数组中的任何元素。
1 | value_type* data() noexcept; |
Example
1 |
|
Output
1 | Test string |
用val填充数组所有元素,将val设置为数组对象中所有元素的值。
1 | void fill (const value_type& val); |
Example
1 |
|
Output
1 | myarray contains: 5 5 5 5 5 5 |
通过x的内容交换数组的内容,这是另一个相同类型的数组对象(包括相同的大小)。
与其他容器的交换成员函数不同,此成员函数通过在各个元素之间执行与其大小相同的单独交换操作,以线性时间运行。
1 | void swap (array& x) noexcept(noexcept(swap(declval<value_type&>(),declval<value_type&>()))); |
Example
1 |
|
Output
1 | first: 11 22 33 44 55 |
形如:std::get<0>(myarray);传入一个数组容器,返回指定位置元素的引用。
1 | template <size_t I,class T,size_t N> T&get(array <T,N>&arr)noexcept; |
Example
1 |
|
Output
1 | first element in myarray: 30 |
形如:arrayA != arrayB、arrayA > arrayB;依此比较数组每个元素的大小关系。
1 | (1) |
Example
1 |
|
Output
1 | a and b are equal |
vector是表示可以改变大小的数组的序列容器。
就像数组一样,vector为它们的元素使用连续的存储位置,这意味着它们的元素也可以使用到其元素的常规指针上的偏移来访问,而且和数组一样高效。但是与数组不同的是,它们的大小可以动态地改变,它们的存储由容器自动处理。
在内部,vector使用一个动态分配的数组来存储它们的元素。这个数组可能需要重新分配,以便在插入新元素时增加大小,这意味着分配一个新数组并将所有元素移动到其中。就处理时间而言,这是一个相对昂贵的任务,因此每次将元素添加到容器时矢量都不会重新分配。
相反,vector容器可以分配一些额外的存储以适应可能的增长,并且因此容器可以具有比严格需要包含其元素(即,其大小)的存储更大的实际容量。库可以实现不同的策略的增长到内存使用和重新分配之间的平衡,但在任何情况下,再分配应仅在对数生长的间隔发生尺寸,使得在所述载体的末端各个元件的插入可以与提供分期常量时间复杂性。
因此,与数组相比,载体消耗更多的内存来交换管理存储和以有效方式动态增长的能力。
与其他动态序列容器(deques,lists和 forward_lists )相比,vector非常有效地访问其元素(就像数组一样),并相对有效地从元素末尾添加或移除元素。对于涉及插入或移除除了结尾之外的位置的元素的操作,它们执行比其他位置更差的操作,并且具有比列表和 forward_lists 更不一致的迭代器和引用。
针对 vector 的各种常见操作的复杂度(效率)如下:
1 | template < class T, class Alloc = allocator<T> > class vector; |
(1)empty容器构造函数(默认构造函数)
构造一个空的容器,没有元素。
(2)fill构造函数
用n个元素构造一个容器。每个元素都是val的副本(如果提供)。
(3)范围(range)构造器
使用与[ range,first,last]范围内的元素相同的顺序构造一个容器,其中的每个元素都是emplace -从该范围内相应的元素构造而成。
(4)复制(copy)构造函数(并用分配器复制)
按照相同的顺序构造一个包含x中每个元素的副本的容器。
(5)移动(move)构造函数(和分配器移动)
构造一个获取x元素的容器。
如果指定了alloc并且与x的分配器不同,那么元素将被移动。否则,没有构建元素(他们的所有权直接转移)。
x保持未指定但有效的状态。
(6)初始化列表构造函数
构造一个容器中的每个元件中的一个拷贝的IL,以相同的顺序。
1 | default (1) |
Example
1 |
|
Output
1 | The contents of fifth are: 16 2 77 29 |
销毁容器对象。这将在每个包含的元素上调用allocator_traits::destroy,并使用其分配器释放由矢量分配的所有存储容量。
1 | ~vector(); |
将新内容分配给容器,替换其当前内容,并相应地修改其大小。
1 | copy (1) |
Example
1 | #include <iostream> |
Output
1 | Size of foo: 0 |
返回vector中元素的数量。
这是vector中保存的实际对象的数量,不一定等于其存储容量。
1 | size_type size() const noexcept; |
Example
1 |
|
Output
1 | 0. size: 0 |
返回该vector可容纳的元素的最大数量。由于已知的系统或库实现限制,
这是容器可以达到的最大潜在大小,但容器无法保证能够达到该大小:在达到该大小之前的任何时间,仍然无法分配存储。
1 | size_type max_size() const noexcept; |
Example
1 |
|
A possible output for this program could be:
1 | size: 100 |
调整容器的大小,使其包含n个元素。
如果n小于当前的容器size,内容将被缩小到前n个元素,将其删除(并销毁它们)。
如果n大于当前容器size,则通过在末尾插入尽可能多的元素以达到大小n来扩展内容。如果指定了val,则新元素将初始化为val的副本,否则将进行值初始化。
如果n也大于当前的容器的capacity(容量),分配的存储空间将自动重新分配。
注意这个函数通过插入或者删除元素的内容来改变容器的实际内容。
1 | void resize (size_type n); |
Example
1 |
|
Output
1 | myvector contains: 1 2 3 4 5 100 100 100 0 0 0 0 |
返回当前为vector分配的存储空间的大小,用元素表示。这个capacity(容量)不一定等于vector的size。它可以相等或更大,额外的空间允许适应增长,而不需要重新分配每个插入。
1 | size_type capacity() const noexcept; |
Example
1 |
|
A possible output for this program could be:
1 | size: 100 |
返回vector是否为空(即,它的size是否为0)
1 | bool empty() const noexcept; |
Example
1 |
|
Output
1 | total: 55 |
请求vector容量至少足以包含n个元素。
如果n大于当前vector容量,则该函数使容器重新分配其存储容量,从而将其容量增加到n(或更大)。
在所有其他情况下,函数调用不会导致重新分配,并且vector容量不受影响。
这个函数对vector大小没有影响,也不能改变它的元素。
1 | void reserve (size_type n); |
Example
1 |
|
Possible output
1 | making foo grow: |
要求容器减小其capacity(容量)以适应其尺寸。
该请求是非绑定的,并且容器实现可以自由地进行优化,并且保持capacity大于其size的vector。 这可能导致重新分配,但对矢量大小没有影响,并且不能改变其元素。
1 | void shrink_to_fit(); |
Example
1 |
|
Possible output
1 | 1. capacity of myvector: 100 |
将新内容分配给vector,替换其当前内容,并相应地修改其大小。
在范围版本(1)中,新内容是从第一个和最后一个范围内的每个元素按相同顺序构造的元素。
在填充版本(2)中,新内容是n个元素,每个元素都被初始化为一个val的副本。
在初始化列表版本(3)中,新内容是以相同顺序作为初始化列表传递的值的副本。
所述内部分配器被用于(通过其性状),以分配和解除分配存储器如果重新分配发生。它也习惯于摧毁所有现有的元素,并构建新的元素。
1 | range (1) |
Example
1 |
|
Output
1 | Size of first: 7 |
补充:vector::assign 与 vector::operator= 的区别:
1 | void assign(size_type __n, const _Tp& __val) { _M_fill_assign(__n, __val); } |
1 | template <class _Tp, class _Alloc> |
在vector的最后一个元素之后添加一个新元素。val的内容被复制(或移动)到新的元素。
这有效地将容器size增加了一个,如果新的矢量size超过了当前vector的capacity,则导致所分配的存储空间自动重新分配。
1 | void push_back (const value_type& val); |
Example
1 |
|
删除vector中的最后一个元素,有效地将容器size减少一个。
这破坏了被删除的元素。
1 | void pop_back(); |
Example
1 |
|
Output
1 | The elements of myvector add up to 600 |
通过在指定位置的元素之前插入新元素来扩展该vector,通过插入元素的数量有效地增加容器大小。 这会导致分配的存储空间自动重新分配,只有在新的vector的size超过当前的vector的capacity的情况下。
由于vector使用数组作为其基础存储,因此除了将元素插入到vector末尾之后,或vector的begin之前,其他位置会导致容器重新定位位置之后的所有元素到他们的新位置。与其他种类的序列容器(例如list或forward_list)执行相同操作的操作相比,这通常是低效的操作。
1 | single element (1) |
Example
1 |
|
Output
1 | myvector contains: 501 502 503 300 300 400 400 200 100 100 100 |
补充:insert 迭代器野指针错误:
1 | int main() |
改正:应该把vi = v.begin();
放到v.push_back(10);
后面
从vector中删除单个元素(position)或一系列元素([first,last))。
这有效地减少了被去除的元素的数量,从而破坏了容器的大小。
由于vector使用一个数组作为其底层存储,所以删除除vector结束位置之后,或vector的begin之前的元素外,将导致容器将段被擦除后的所有元素重新定位到新的位置。与其他种类的序列容器(例如list或forward_list)执行相同操作的操作相比,这通常是低效的操作。
1 | iterator erase (const_iterator position); |
Example
1 |
|
Output
1 | myvector contains: 4 5 7 8 9 10 |
通过x的内容交换容器的内容,x是另一个相同类型的vector对象。尺寸可能不同。
在调用这个成员函数之后,这个容器中的元素是那些在调用之前在x中的元素,而x的元素是在这个元素中的元素。所有迭代器,引用和指针对交换对象保持有效。
请注意,非成员函数存在具有相同名称的交换,并使用与此成员函数相似的优化来重载该算法。
1 | void swap (vector& x); |
Example
1 |
|
Output
1 | foo contains: 200 200 200 200 200 |
从vector中删除所有的元素(被销毁),留下size为0的容器。
不保证重新分配,并且由于调用此函数, vector的capacity不保证发生变化。强制重新分配的典型替代方法是使用swap:vector<T>().swap(x); // clear x reallocating
1 | void clear() noexcept; |
Example
1 |
|
Output
1 | 50 50 50 50 50 |
通过在position位置处插入新元素args来扩展容器。这个新元素是用args作为构建的参数来构建的。
这有效地增加了一个容器的大小。
分配存储空间的自动重新分配发生在新的vector的size超过当前向量容量的情况下。
由于vector使用数组作为其基础存储,因此除了将元素插入到vector末尾之后,或vector的begin之前,其他位置会导致容器重新定位位置之后的所有元素到他们的新位置。与其他种类的序列容器(例如list或forward_list)执行相同操作的操作相比,这通常是低效的操作。
该元素是通过调用allocator_traits::construct来转换args来创建的。插入一个类似的成员函数,将现有对象复制或移动到容器中。
1 | template <class... Args> |
Example
1 |
|
Output
1 | myvector contains: 10 200 100 20 30 300 |
在vector的末尾插入一个新的元素,紧跟在当前的最后一个元素之后。这个新元素是用args作为构造函数的参数来构造的。
这有效地将容器大小增加了一个,如果新的矢量大小超过了当前的vector容量,则导致所分配的存储空间自动重新分配。
该元素是通过调用allocator_traits :: construct来转换args来创建的。
与push_back相比,emplace_back可以避免额外的复制和移动操作。
1 | template <class... Args> |
Example
1 |
|
Output
1 | emplace_back: |
返回与vector关联的构造器对象的副本。
1 | allocator_type get_allocator() const noexcept; |
Example
1 |
|
Output
1 | The allocated array contains: 0 1 2 3 4 |
deque([‘dek])(双端队列)是double-ended queue 的一个不规则缩写。deque是具有动态大小的序列容器,可以在两端(前端或后端)扩展或收缩。
特定的库可以以不同的方式实现deques,通常作为某种形式的动态数组。但是在任何情况下,它们都允许通过随机访问迭代器直接访问各个元素,通过根据需要扩展和收缩容器来自动处理存储。
因此,它们提供了类似于vector的功能,但是在序列的开始部分也可以高效地插入和删除元素,而不仅仅是在结尾。但是,与vector不同,deques并不保证将其所有元素存储在连续的存储位置:deque通过偏移指向另一个元素的指针访问元素会导致未定义的行为。
两个vector和deques提供了一个非常相似的接口,可以用于类似的目的,但内部工作方式完全不同:虽然vector使用单个数组需要偶尔重新分配以增长,但是deque的元素可以分散在不同的块的容器,容器在内部保存必要的信息以提供对其任何元素的持续时间和统一的顺序接口(通过迭代器)的直接访问。因此,deques在内部比vector更复杂一点,但是这使得他们在某些情况下更有效地增长,尤其是在重新分配变得更加昂贵的很长序列的情况下。
对于频繁插入或删除开始或结束位置以外的元素的操作,deques表现得更差,并且与列表和转发列表相比,迭代器和引用的一致性更低。
deque上常见操作的复杂性(效率)如下:
1 | template < class T, class Alloc = allocator<T> > class deque; |
构造一个deque容器对象,根据所使用的构造函数版本初始化它的内容:
Example
1 |
|
Output
1 | The contents of fifth are: 16 2 77 29 |
在当前的最后一个元素之后 ,在deque容器的末尾添加一个新元素。val的内容被复制(或移动)到新的元素。
这有效地增加了一个容器的大小。
1 | void push_back (const value_type& val); |
Example
1 |
|
在deque容器的开始位置插入一个新的元素,位于当前的第一个元素之前。val的内容被复制(或移动)到插入的元素。
这有效地增加了一个容器的大小。
1 | void push_front (const value_type& val); |
Example
1 |
|
Output
1 | 300 200 100 100 |
删除deque容器中的最后一个元素,有效地将容器大小减少一个。
这破坏了被删除的元素。
1 | void pop_back(); |
Example
1 |
|
Output
1 | The elements of mydeque add up to 60 |
删除deque容器中的第一个元素,有效地减小其大小。
这破坏了被删除的元素。
1 | void pop_front(); |
Example
1 |
|
Output
1 | Popping out the elements in mydeque: 100 200 300 |
在deque的开头插入一个新的元素,就在其当前的第一个元素之前。这个新的元素是用args作为构建的参数来构建的。
这有效地增加了一个容器的大小。
该元素是通过调用allocator_traits::construct来转换args来创建的。
存在一个类似的成员函数push_front,它可以将现有对象复制或移动到容器中。
1 | template <class... Args> |
Example
1 |
|
Output
1 | mydeque contains: 222 111 10 20 30 |
在deque的末尾插入一个新的元素,紧跟在当前的最后一个元素之后。这个新的元素是用args作为构建的参数来构建的。
这有效地增加了一个容器的大小。
该元素是通过调用allocator_traits::construct来转换args来创建的。
存在一个类似的成员函数push_back,它可以将现有对象复制或移动到容器中
1 | template <class... Args> |
Example
1 |
|
Output
1 | mydeque contains: 10 20 30 100 200 |
forward_list(单向链表)是序列容器,允许在序列中的任何地方进行恒定的时间插入和擦除操作。
forward_list(单向链表)被实现为单链表; 单链表可以将它们包含的每个元素存储在不同和不相关的存储位置中。通过关联到序列中下一个元素的链接的每个元素来保留排序。forward_list容器和列表
之间的主要设计区别容器是第一个内部只保留一个到下一个元素的链接,而后者每个元素保留两个链接:一个指向下一个元素,一个指向前一个元素,允许在两个方向上有效的迭代,但是每个元素消耗额外的存储空间并且插入和移除元件的时间开销略高。因此,forward_list对象比列表对象更有效率,尽管它们只能向前迭代。
与其他基本的标准序列容器(array,vector和deque),forward_list通常在插入,提取和移动容器内任何位置的元素方面效果更好,因此也适用于密集使用这些元素的算法,如排序算法。
的主要缺点修饰符Modifiers S和列表相比这些其它序列容器s是说,他们缺乏可以通过位置的元素的直接访问; 例如,要访问forward_list中的第六个元素,必须从开始位置迭代到该位置,这需要在这些位置之间的线性时间。它们还消耗一些额外的内存来保持与每个元素相关联的链接信息(这可能是大型小元素列表的重要因素)。
该修饰符Modifiersclass模板的设计考虑到效率:按照设计,它与简单的手写C型单链表一样高效,实际上是唯一的标准容器,为了效率的考虑故意缺少尺寸成员函数:由于其性质作为一个链表,具有一个需要一定时间的大小的成员将需要它保持一个内部计数器的大小(如列表所示)。这会消耗一些额外的存储空间,并使插入和删除操作效率稍低。要获取forward_list对象的大小,可以使用距离算法的开始和结束,这是一个需要线性时间的操作。
1 | default (1) |
Example
1 |
|
Possible output
1 | forward_list constructor examples: |
返回指向容器中第一个元素之前的位置的迭代器。
返回的迭代器不应被解除引用:它是为了用作成员函数的参数emplace_after,insert_after,erase_after或splice_after,指定序列,其中执行该动作的位置的开始位置。
1 | iterator before_begin() noexcept; |
Example
1 |
|
Output
1 | mylist contains: 11 20 30 40 50 |
返回指向容器中第一个元素之前的位置的const_iterator。
一个常量性是指向常量内容的迭代器。这个迭代器可以增加和减少(除非它本身也是const),就像forward_list::before_begin返回的迭代器一样,但不能用来修改它指向的内容。
返回的价值不得解除引用。
1 | const_iterator cbefore_begin() const noexcept; |
Example
1 |
|
Output
1 | mylist contains: 19 77 2 16 |
map 是关联容器,按照特定顺序存储由 key value (键值) 和 mapped value (映射值) 组合形成的元素。
在映射中,键值通常用于对元素进行排序和唯一标识,而映射的值存储与此键关联的内容。该类型的键和映射的值可能不同,并且在部件类型被分组在一起VALUE_TYPE,这是一种对类型结合两种:
1 | typedef pair<const Key, T> value_type; |
在内部,映射中的元素总是按照由其内部比较对象(比较类型)指示的特定的严格弱排序标准按键排序。映射容器通常比unordered_map容器慢,以通过它们的键来访问各个元素,但是它们允许基于它们的顺序对子集进行直接迭代。 在该映射值地图可以直接通过使用其相应的键来访问括号运算符((操作符[] )。 映射通常如实施
1 | template < class Key, // map::key_type |
构造一个映射容器对象,根据所使用的构造器版本初始化其内容:
(1)空容器构造函数(默认构造函数)
构造一个空的容器,没有元素。
(2)范围构造函数
构造具有一样多的元素的范围内的容器[第一,最后一个),其中每个元件布设构造的从在该范围内它的相应的元件。
(3)复制构造函数(并用分配器复制)
使用x中的每个元素的副本构造一个容器。
(4)移动构造函数(并与分配器一起移动)
构造一个获取x元素的容器。
如果指定了alloc并且与x的分配器不同,那么元素将被移动。否则,没有构建元素(他们的所有权直接转移)。
x保持未指定但有效的状态。
(5)初始化列表构造函数
用il中的每个元素的副本构造一个容器。
1 | empty (1) |
Example
1 |
|
返回引用map容器中第一个元素的迭代器。
由于map容器始终保持其元素的顺序,所以开始指向遵循容器排序标准的元素。
如果容器是空的,则返回的迭代器值不应被解除引用。
1 | iterator begin() noexcept; |
Example
1 |
|
Output
1 | a => 200 |
返回容器用于比较键的比较对象的副本。
1 | key_compare key_comp() const; |
Example
1 |
|
Output
1 | mymap contains: |
返回可用于比较两个元素的比较对象,以获取第一个元素的键是否在第二个元素之前。
1 | value_compare value_comp() const; |
Example
1 |
|
Output
1 | mymap contains: |
在容器中搜索具有等于k的键的元素,如果找到则返回一个迭代器,否则返回map::end的迭代器。
如果容器的比较对象自反地返回假(即,不管元素作为参数传递的顺序),则两个key被认为是等同的。
另一个成员函数map::count可以用来检查一个特定的键是否存在。
1 | iterator find (const key_type& k); |
Example
1 |
|
Output
1 | elements in mymap: |
在容器中搜索具有等于k的键的元素,并返回匹配的数量。
由于地图容器中的所有元素都是唯一的,因此该函数只能返回1(如果找到该元素)或返回零(否则)。
如果容器的比较对象自反地返回错误(即,不管按键作为参数传递的顺序),则两个键被认为是等同的。
1 | size_type count (const key_type& k) const; |
Example
1 |
|
Output
1 | a is an element of mymap. |
将迭代器返回到下限
返回指向容器中第一个元素的迭代器,该元素的键不会在k之前出现(即,它是等价的或者在其后)。
该函数使用其内部比较对象(key_comp)来确定这一点,将迭代器返回到key_comp(element_key,k)将返回false的第一个元素。
如果map类用默认的比较类型(less)实例化,则函数返回一个迭代器到第一个元素,其键不小于k。
一个类似的成员函数upper_bound具有相同的行为lower_bound,除非映射包含一个key值等于k的元素:在这种情况下,lower_bound返回指向该元素的迭代器,而upper_bound返回指向下一个元素的迭代器。
1 | iterator lower_bound (const key_type& k); |
Example
1 |
|
Output
1 | a => 20 |
将迭代器返回到上限
返回一个指向容器中第一个元素的迭代器,它的关键字被认为是在k之后。
该函数使用其内部比较对象(key_comp)来确定这一点,将迭代器返回到key_comp(k,element_key)将返回true的第一个元素。
如果map类用默认的比较类型(less)实例化,则函数返回一个迭代器到第一个元素,其键大于k。
类似的成员函数lower_bound具有与upper_bound相同的行为,除了map包含一个元素,其键值等于k:在这种情况下,lower_bound返回指向该元素的迭代器,而upper_bound返回指向下一个元素的迭代器。
1 | iterator upper_bound (const key_type& k); |
Example
1 |
|
Output
1 | a => 20 |
获取相同元素的范围
返回包含容器中所有具有与k等价的键的元素的范围边界。 由于地图容器中的元素具有唯一键,所以返回的范围最多只包含一个元素。
如果没有找到匹配,则返回的范围具有零的长度,与两个迭代器指向具有考虑去后一个密钥对所述第一元件ķ根据容器的内部比较对象(key_comp)。如果容器的比较对象返回false,则两个键被认为是等价的。
1 | pair<const_iterator,const_iterator> equal_range (const key_type& k) const; |
Example
1 |
|
Output
1 | lower bound points to: 'b' => 20 |
包括:
都是以哈希表实现的。
unordered_set、unodered_multiset结构:
unordered_map、unodered_multimap结构:
元组是一个能够容纳元素集合的对象。每个元素可以是不同的类型。
1 | template <class... Types> class tuple; |
Example
1 |
|
Output
1 | foo contains: 100 y |
构建一个 tuple(元组)对象。
这涉及单独构建其元素,初始化取决于调用的构造函数形式:
(1)默认的构造函数
构建一个 元组对象的元素值初始化。
(2)复制/移动构造函数
该对象使用tpl的内容进行初始化 元组目的。tpl
的相应元素被传递给每个元素的构造函数。
(3)隐式转换构造函数
同上。tpl中的
所有类型都可以隐含地转换为构造中它们各自元素的类型元组 目的。
(4)初始化构造函数
用elems中的相应元素初始化每个元素。elems
的相应元素被传递给每个元素的构造函数。
(5)对转换构造函数
该对象有两个对应于pr.first和的元素pr.second。PR中的所有类型都应该隐含地转换为其中各自元素的类型元组 目的。
(6)分配器版本
和上面的版本一样,除了每个元素都是使用allocator alloc构造的。
1 | default (1) |
Example
1 |
|
Output
1 | sixth contains: 30 and c |
这个类把一对值(values)结合在一起,这些值可能是不同的类型(T1 和 T2)。每个值可以被公有的成员变量first、second访问。
pair是tuple(元组)的一个特例。
pair的实现是一个结构体,主要的两个成员变量是first second 因为是使用struct不是class,所以可以直接使用pair的成员变量。
应用:
1 | template <class T1, class T2> struct pair; |
构建一个pair对象。
这涉及到单独构建它的两个组件对象,初始化依赖于调用的构造器形式:
(1)默认的构造函数
构建一个 对对象的元素值初始化。
(2)复制/移动构造函数(和隐式转换)
该对象被初始化为pr的内容 对目的。pr
的相应成员被传递给每个成员的构造函数。
(3)初始化构造函数
会员 第一是由一个和成员构建的第二与b。
(4)分段构造
构造成员 first 和 second 到位,传递元素first_args 作为参数的构造函数 first,和元素 second_args 到的构造函数 second 。
1 | default (1) |
Example
1 |
|
Output
1 | The price of lightbulbs is $0.99 |
C/C++ 面试知识总结,只为复习、分享。部分知识点与图片来自网络,侵删。
勘误新增请 Issue、PR,建议、讨论请 #issues/12,排版使用 中文文案排版指北
使用建议:
Ctrl + F
:快速查找定位知识点TOC 导航
:jawil/GayHub 插件快速目录跳转1 | // 类 |
C++ static 和const 的作用
this
指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向正在被该成员函数操作的那个对象。this
指针,然后调用成员函数,每次成员函数存取数据成员时,由隐含使用 this
指针。this
指针被隐含地声明为: ClassName *const this
,这意味着不能给 this
指针赋值;在 ClassName
类的 const
成员函数中,this
指针的类型为:const ClassName* const
,这说明不能对 this
指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);this
并不是一个常规变量,而是个右值,所以不能取得 this
的地址(不能 &this
)。this
指针:list
。1 | // 声明1(加 inline,建议使用) |
优点
缺点
Are “inline virtual” member functions ever actually “inlined”?
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()
),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。1 |
|
断言,是宏,而非函数。assert 宏的原型定义在 <assert.h>
(C)、<cassert>
(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG
来关闭 assert,但是需要在源代码的开头,include <assert.h>
之前。
1 |
|
设定结构体、联合以及类成员变量以 n 字节方式对齐
1 |
|
1 | Bit mode: 2; // mode 占 2 位 |
类可以将其(非静态)数据成员定义为位域(bit-field),在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
1 | volatile int i = 10; |
extern "C"
修饰的变量和函数是按照 C 语言方式编译和连接的extern "C"
的作用是让 C++ 编译器将 extern "C"
声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。
1 |
|
1 | // c |
等价于
1 | // c |
此时 S
等价于 struct Student
,但两个标识符名称空间不相同。
另外还可以定义与 struct Student
不冲突的 void Student() {}
。
由于编译器定位符号的规则(搜索规则)改变,导致不同于C语言。
一、如果在类标识符空间定义了 struct Student {...};
,使用 Student me;
时,编译器将搜索全局标识符表,Student
未找到,则在类标识符内搜索。
即表现为可以使用 Student
也可以使用 struct Student
,如下:
1 | // cpp |
二、若定义了与 Student
同名函数之后,则 Student
只代表函数,不代表结构体,如下:
1 | typedef struct Student { |
总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
1 |
|
C 语言实现封装、继承和多态
explicit 修饰的构造函数可用来防止隐式转换
1 | class Test1 |
一条 using 声明
语句一次只引入命名空间的一个成员。它使得我们可以清楚知道程序中所引用的到底是哪个名字。如:
1 | using namespace_name::name; |
在 C++11 中,派生类能够重用其直接基类定义的构造函数。
1 | class Derived : Base { |
如上 using 声明,对于基类的每个构造函数,编译器都生成一个与之对应(形参列表完全相同)的派生类构造函数。生成如下类型构造函数:
1 | derived(parms) : base(args) { } |
using 指示
使得某个特定命名空间中所有名字都可见,这样我们就无需再为它们添加任何前缀限定符了。如:
1 | using namespace_name name; |
using 指示
污染命名空间一般说来,使用 using 命令比使用 using 编译命令更安全,这是由于它只导入了制定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译命令导入所有的名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。
尽量少使用 using 指示
1 | using namespace std; |
应该多使用 using 声明
1 | int x; |
或者
1 | using std::cin; |
::name
):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间class::name
):用于表示指定类型的作用域范围是具体某个类的namespace::name
):用于表示指定类型的作用域范围是具体某个命名空间的1 | int count = 0; // 全局(::)的 count |
1 | enum class open_modes { input, output, append }; |
1 | enum color { red, yellow, green }; |
C++ 枚举类型详解
decltype 关键字用于检查实体的声明类型或表达式的类型及值分类。语法:
1 | decltype ( expression ) |
1 | // 尾置返回允许我们在参数列表之后声明返回类型 |
常规引用,一般表示对象的身份。
右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。
右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:
X& &
、X& &&
、X&& &
可折叠成 X&
X&& &&
可折叠成 X&&
详解c++ 引用(reference)与 指针(pointer)的区别与联系
好处
用花括号初始化器列表列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list
参数.
1 |
|
面向对象程序设计(Object-oriented programming,OOP)是种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。
面向对象三大特征 —— 封装、继承、多态
关键字 | 当前类 | 包内 | 子孙类 | 包外 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
friendly | √ | √ | × | × |
private | √ | × | × | × |
函数重载
1 | class A |
注意:
1 | class Shape // 形状类 |
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
1 | class Shape |
纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。
1 | virtual int A() = 0; |
CSDN . C++ 中的虚函数、纯虚函数区别和联系
.rodata section
,见:目标文件存储结构),存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。
底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
C/C++内存管理详解 - ShinChan’s Blog
用于分配、释放内存
申请内存,确认是否申请成功
1 | char *str = (char*) malloc(100); |
释放内存后指针置空
1 | free(p); |
申请内存,确认是否申请成功
1 | int main() |
定位 new(placement new)允许我们向 new 传递额外的参数。
1 | new (palce_address) type |
palce_address
是个指针initializers
提供一个(可能为空的)以逗号分隔的初始值列表Is it legal (and moral) for a member function to say delete this?
合法,但:
new
(不是 new[]
、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的delete this
的成员函数是最后一个调用 this 的成员函数delete this
后面没有调用 this 了delete this
后没有人使用了如何定义一个只能在堆上(栈上)生成对象的类?
方法:将析构函数设置为私有
原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
方法:将 new 和 delete 重载为私有
原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。
C++11及C++14标准的智能指针
C++ 智能指针
使用 C++11 智能指针时要避开的 10 大错误
头文件:#include <memory>
1 | std::auto_ptr<std::string> ps (new std::string(str)); |
多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。
weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。
unique_ptr 是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有着被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。
被 c++11 弃用,原因是缺乏语言特性如 “针对构造和赋值” 的 std::move
语义,以及其他瑕疵。
move
语义;delete
),unique_ptr 可以管理数组(析构调用 delete[]
);MSDN . 强制转换运算符
C++类型转换总结
向上转换是一种隐式转换。
char*
到 int*
或 One_class*
到 Unrelated_class*
之类的转换,但其本身并不安全)1 | try { |
typeinfo
1 | class Flyable // 能飞的 |
const
、enum
、inline
替换 #define
)operator=
返回一个 reference to *this
(用于连锁赋值)operator=
中处理 “自我赋值”new
中使用 []
则 delete []
,new
中不使用 []
则 delete
)(T)expression
、T(expression)
;新式:const_cast<T>(expression)
、dynamic_cast<T>(expression)
、reinterpret_cast<T>(expression)
、static_cast<T>(expression)
、;尽量避免转型、注重效率避免 dynamic_casts、尽量设计成无需转型、可把转型封装成函数、宁可用新式转型)tr1::function
成员变量替换 virtual 函数,将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数)英文:Google C++ Style Guide
中文:C++ 风格指南
图片来源于:CSDN . 一张图总结Google C++编程规范(Google C++ Style Guide)
STL 方法含义索引
C++ STL容器总结
容器 | 底层数据结构 | 时间复杂度 | 有无序 | 可不可重复 | 其他 |
---|---|---|---|---|---|
array | 数组 | 随机读改 O(1) | 无序 | 可重复 | 支持快速随机访问 |
vector | 数组 | 随机读改、尾部插入、尾部删除 O(1) 头部插入、头部删除 O(n) | 无序 | 可重复 | 支持快速随机访问 |
list | 双向链表 | 插入、删除 O(1) 随机读改 O(n) | 无序 | 可重复 | 支持快速增删 |
deque | 双端队列 | 头尾插入、头尾删除 O(1) | 无序 | 可重复 | 一个中央控制器 + 多个缓冲区,支持首尾快速增删,支持随机访问 |
stack | deque / list | 顶部插入、顶部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 |
queue | deque / list | 尾部插入、头部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 |
priority_queue | vector + max-heap | 插入、删除 O(log2n) | 有序 | 可重复 | vector容器+heap处理规则 |
set | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | |
multiset | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 | |
map | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | |
multimap | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 | |
hash_set | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 不可重复 | |
hash_multiset | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 可重复 | |
hash_map | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 不可重复 | |
hash_multimap | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 可重复 |
算法 | 底层算法 | 时间复杂度 | 可不可重复 |
---|---|---|---|
find | 顺序查找 | O(n) | 可重复 |
sort | 内省排序 | O(n*log2n) | 可重复 |
1 | typedef struct { |
1 | typedef struct { |
SqQueue.rear++
SqQueue.rear = (SqQueue.rear + 1) % SqQueue.maxSize
1 | typedef struct { |
1 | typedef struct LNode { |
哈希函数:H(key): K -> D , key ∈ K
Hi = (H(key) + i) % m
Di = 1^2, -1^2, ..., ±(k)^2,(k<=m/2)
H = (H(key) + 伪随机数) % m
1 | typedef char KeyType; |
函数直接或间接地调用自身
1 | // 广义表的头尾链表存储表示 |
1 | // 广义表的扩展线性链表存储表示 |
1 | typedef struct BiTNode |
一种不相交的子集所构成的集合 S = {S1, S2, …, Sn}
F(n)=F(n-1)+F(n-2)+1
(1 是根节点,F(n-1) 是左子树的节点数量,F(n-2) 是右子树的节点数量)平衡二叉树插入新结点导致失衡的子树
调整:
对于在内部节点的数据,可直接得到,不必根据叶子节点来定位。
B 树、B+ 树区别来自:differences-between-b-trees-and-b-trees、B树和B+树的区别
八叉树(octree),或称八元树,是一种用于描述三维空间(划分空间)的树状数据结构。八叉树的每个节点表示一个正方体的体积元素,每个节点有八个子节点,这八个子节点所表示的体积元素加在一起就等于父节点的体积。一般中心点作为节点的分叉中心。
排序算法 | 平均时间复杂度 | 最差时间复杂度 | 空间复杂度 | 数据对象稳定性 |
---|---|---|---|---|
冒泡排序 | O(n2) | O(n2) | O(1) | 稳定 |
选择排序 | O(n2) | O(n2) | O(1) | 数组不稳定、链表稳定 |
插入排序 | O(n2) | O(n2) | O(1) | 稳定 |
快速排序 | O(n*log2n) | O(n2) | O(log2n) | 不稳定 |
堆排序 | O(n*log2n) | O(n*log2n) | O(1) | 不稳定 |
归并排序 | O(n*log2n) | O(n*log2n) | O(n) | 稳定 |
希尔排序 | O(n*log2n) | O(n2) | O(1) | 不稳定 |
计数排序 | O(n+m) | O(n+m) | O(n+m) | 稳定 |
桶排序 | O(n) | O(n) | O(m) | 稳定 |
基数排序 | O(k*n) | O(n2) | 稳定 |
- 均按从小到大排列
- k:代表数值中的 “数位” 个数
- n:代表数据规模
- m:代表数据的最大值减最小值
- 来自:wikipedia . 排序算法
查找算法 | 平均时间复杂度 | 空间复杂度 | 查找条件 |
---|---|---|---|
顺序查找 | O(n) | O(1) | 无序或有序 |
二分查找(折半查找) | O(log2n) | O(1) | 有序 |
插值查找 | O(log2(log2n)) | O(1) | 有序 |
斐波那契查找 | O(log2n) | O(1) | 有序 |
哈希查找 | O(1) | O(n) | 无序或有序 |
二叉查找树(二叉搜索树查找) | O(log2n) | ||
红黑树 | O(log2n) | ||
2-3树 | O(log2n - log3n) | ||
B树/B+树 | O(log2n) |
图搜索算法 | 数据结构 | 遍历时间复杂度 | 空间复杂度 |
---|---|---|---|
BFS广度优先搜索 | 邻接矩阵 邻接链表 | O(|v|2) O(|v|+|E|) | O(|v|2) O(|v|+|E|) |
DFS深度优先搜索 | 邻接矩阵 邻接链表 | O(|v|2) O(|v|+|E|) | O(|v|2) O(|v|+|E|) |
算法 | 思想 | 应用 |
---|---|---|
分治法 | 把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并 | 循环赛日程安排问题、排序算法(快速排序、归并排序) |
动态规划 | 通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,适用于有重叠子问题和最优子结构性质的问题 | 背包问题、斐波那契数列 |
贪心法 | 一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法 | 旅行推销员问题(最短路径问题)、最小生成树、哈夫曼编码 |
【构建操作系统】进程间通信
C++ 高性能服务器网络框架设计细节
epoll编程,如何实现高并发服务器开发?
如何实现高并发服务器开发
编程思想之多线程与多进程(4)——C++中的多线程
对于有线程系统:
对于无线程系统:
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制
进程之间的通信方式以及优缺点来源于:进程线程面试题总结
对比维度 | 多进程 | 多线程 | 总结 |
---|---|---|---|
数据共享、同步 | 数据共享复杂,需要用 IPC;数据是分开的,同步简单 | 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 | 各有优势 |
内存、CPU | 占用内存多,切换复杂,CPU 利用率低 | 占用内存少,切换简单,CPU 利用率高 | 线程占优 |
创建销毁、切换 | 创建销毁、切换复杂,速度慢 | 创建销毁、切换简单,速度很快 | 线程占优 |
编程、调试 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程占优 |
可靠性 | 进程间不会互相影响 | 一个线程挂掉将导致整个进程挂掉 | 进程占优 |
分布式 | 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 | 适应于多核分布式 | 进程占优 |
优劣 | 多进程 | 多线程 |
---|---|---|
优点 | 编程、调试简单,可靠性较高 | 创建、销毁、切换速度快,内存、资源占用小 |
缺点 | 创建、销毁、切换速度慢,内存、资源占用大 | 编程、调试复杂,可靠性较差 |
多进程与多线程间的对比、优劣与选择来自:多线程还是多进程的选择及区别
虽然通常可以在进程之间共享内存,但这难以建立并且通常难以管理,因为同一数据的内存地址在不同的进程中也不尽相同。
共享内存的灵活性是有代价的:如果数据要被多个线程访问,那么必须确保当每个线程访问时所看到的数据是一致的。
在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实象多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问。尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问。
来自:Linux 内核的同步机制,第 1 部分、Linux 内核的同步机制,第 2 部分
主机字节序又叫 CPU 字节序,其不是由操作系统决定的,而是由 CPU 指令集架构决定的。主机字节序分为两种:
32 位整数 0x12345678
是从起始位置为 0x00
的地址开始存放,则:
内存地址 | 0x00 | 0x01 | 0x02 | 0x03 |
---|---|---|---|---|
大端 | 12 | 34 | 56 | 78 |
小端 | 78 | 56 | 34 | 12 |
可以这样判断自己 CPU 字节序是大端还是小端:
1 |
|
网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保重数据在不同主机之间传输时能够被正确解释。
网络字节顺序采用:大端(Big Endian)排列方式。
在地址映射过程中,若在页面中发现所要访问的页面不在内存中,则产生缺页中断。当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。
全局:
局部:
计算机经网络体系结构:
分层 | 作用 | 协议 |
---|---|---|
物理层 | 通过媒介传输比特,确定机械及电气规范(比特 Bit) | RJ45、CLOCK、IEEE802.3(中继器,集线器) |
数据链路层 | 将比特组装成帧和点到点的传递(帧 Frame) | PPP、FR、HDLC、VLAN、MAC(网桥,交换机) |
网络层 | 负责数据包从源到宿的传递和网际互连(包 Packet) | IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGRP(路由器) |
运输层 | 提供端到端的可靠报文传递和错误恢复( 段Segment) | TCP、UDP、SPX |
会话层 | 建立、管理和终止会话(会话协议数据单元 SPDU) | NFS、SQL、NETBIOS、RPC |
表示层 | 对数据进行翻译、加密和压缩(表示协议数据单元 PPDU) | JPEG、MPEG、ASII |
应用层 | 允许访问OSI环境的手段(应用协议数据单元 APDU) | FTP、DNS、Telnet、SMTP、HTTP、WWW、NFS |
通道:
通道复用技术:
主要信道:
三个基本问题:
SOH - 数据部分 - EOT
点对点协议(Point-to-Point Protocol):
广播通信:
IP 地址分类:
IP 地址 ::= {<网络号>,<主机号>}
IP 地址类别 | 网络号 | 网络范围 | 主机号 | IP 地址范围 |
---|---|---|---|---|
A 类 | 8bit,第一位固定为 0 | 0 —— 127 | 24bit | 1.0.0.0 —— 127.255.255.255 |
B 类 | 16bit,前两位固定为 10 | 128.0 —— 191.255 | 16bit | 128.0.0.0 —— 191.255.255.255 |
C 类 | 24bit,前三位固定为 110 | 192.0.0 —— 223.255.255 | 8bit | 192.0.0.0 —— 223.255.255.255 |
D 类 | 前四位固定为 1110,后面为多播地址 | |||
E 类 | 前五位固定为 11110,后面保留为今后所用 |
IP 数据报格式:
ICMP 报文格式:
应用:
0.0.0.0
, Netmask: 0.0.0.0
)指向自治系统的出口。根据应用和执行的不同,路由表可能含有如下附加信息:
协议:
端口:
应用程序 | FTP | TELNET | SMTP | DNS | TFTP | HTTP | HTTPS | SNMP |
---|---|---|---|---|---|---|---|---|
端口号 | 21 | 23 | 25 | 53 | 69 | 80 | 443 | 161 |
特征:
TCP 如何保证可靠传输:
TCP 报文结构
TCP 首部
TCP:状态控制码(Code,Control Flag),占 6 比特,含义如下:
URG=1
时,表明紧急指针字段有效,代表该封包为紧急封包。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据), 且上图中的 Urgent Pointer 字段也会被启用。ACK=1
时确认号字段才有效,代表这个封包为确认封包。当 ACK=0
时,确认号无效。RST=1
时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。FIN=1
时,表明此报文段的发送端的数据已发送完毕,并要求释放运输连接。特征:
UDP 报文结构
UDP 首部
TCP/UDP 图片来源于:https://github.com/JerryC8080/understand-tcp-udp
TCP 是一个基于字节流的传输服务(UDP 基于报文的),“流” 意味着 TCP 所传输的数据是没有边界的。所以可能会出现两个数据包黏在一起的情况。
\r\n
标记。FTP 协议正是这么做的。但问题在于如果数据正文中也含有 \r\n
,则会误判为消息的边界。流量控制(flow control)就是让发送方的发送速率不要太快,要让接收方来得及接收。
拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。
因为 TCP 三次握手建立连接、四次挥手释放连接很重要,所以附上《计算机网络(第 7 版)-谢希仁》书中对此章的详细描述:https://github.com/huihut/interview/blob/master/images/TCP-transport-connection-management.png
【TCP 建立连接全过程解释】
【答案一】因为信道不可靠,而 TCP 想在不可靠信道上建立可靠地传输,那么三次通信是理论上的最小值。(而 UDP 则不需建立可靠传输,因此 UDP 不需要三次握手。)
Google Groups . TCP 建立连接为什么是三次握手?{技术}{网络通信}
【答案二】因为双方都需要确认对方收到了自己发送的序列号,确认过程最少要进行三次通信。
知乎 . TCP 为什么是三次握手,而不是两次或四次?
【答案三】为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
《计算机网络(第 7 版)-谢希仁》
【TCP 释放连接全过程解释】
【问题一】TCP 为什么要进行四次挥手? / 为什么 TCP 建立连接需要三次,而释放连接则需要四次?
【答案一】因为 TCP 是全双工模式,客户端请求关闭连接后,客户端向服务端的连接关闭(一二次挥手),服务端继续传输之前没传完的数据给客户端(数据传输),服务端向客户端的连接关闭(三四次挥手)。所以 TCP 释放连接时服务器的 ACK 和 FIN 是分开发送的(中间隔着数据传输),而 TCP 建立连接时服务器的 ACK 和 SYN 是一起发送的(第二次握手),所以 TCP 建立连接需要三次,而释放连接则需要四次。
【问题二】为什么 TCP 连接时可以 ACK 和 SYN 一起发送,而释放时则 ACK 和 FIN 分开发送呢?(ACK 和 FIN 分开是指第二次和第三次挥手)
【答案二】因为客户端请求释放时,服务器可能还有数据需要传输给客户端,因此服务端要先响应客户端 FIN 请求(服务端发送 ACK),然后数据传输,传输完成后,服务端再提出 FIN 请求(服务端发送 FIN);而连接时则没有中间的数据传输,因此连接时可以 ACK 和 SYN 一起发送。
【问题三】为什么客户端释放最后需要 TIME-WAIT 等待 2MSL 呢?
【答案三】
Time-wait状态(2MSL)一些理解
TCP和UDP详解
HTTP、TCP、UDP详解
域名:
域名 ::= {<三级域名>.<二级域名>.<顶级域名>}
,如:blog.huihut.com
TELNET 协议是 TCP/IP 协议族中的一员,是 Internet 远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。
HTTP(HyperText Transfer Protocol,超文本传输协议)是用于从 WWW(World Wide Web,万维网)服务器传输超文本到本地浏览器的传送协议。
SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。
Socket 建立网络通信连接至少要一对端口号(Socket)。Socket 本质是编程接口(API),对 TCP/IP 的封装,TCP/IP 也要提供可供程序员做网络开发所用的接口,这就是 Socket 编程接口。
标准格式:
协议类型:[//服务器地址[:端口号]][/资源层级UNIX文件路径]文件名[?查询][#片段ID]
完整格式:
协议类型:[//[访问资源需要的凭证信息@]服务器地址[:端口号]][/资源层级UNIX文件路径]文件名[?查询][#片段ID]
其中【访问凭证信息@;:端口号;?查询;#片段ID】都属于选填项
如:https://github.com/huihut/interview#cc
HTTP(HyperText Transfer Protocol,超文本传输协议)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP 是万维网的数据通信的基础。
请求方法
方法 | 意义 |
---|---|
OPTIONS | 请求一些选项信息,允许客户端查看服务器的性能 |
GET | 请求指定的页面信息,并返回实体主体 |
HEAD | 类似于 get 请求,只不过返回的响应中没有具体的内容,用于获取报头 |
POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改 |
PUT | 从客户端向服务器传送的数据取代指定的文档的内容 |
DELETE | 请求服务器删除指定的页面 |
TRACE | 回显服务器收到的请求,主要用于测试或诊断 |
状态码(Status-Code)
更多状态码:菜鸟教程 . HTTP状态码
Linux Socket 编程(不限 Linux)
1 | ssize_t read(int fd, void *buf, size_t count); |
我们知道 TCP 建立连接要进行 “三次握手”,即交换三个分组。大致流程如下:
只有就完了三次握手,但是这个三次握手发生在 Socket 的那几个函数中呢?请看下图:
从图中可以看出:
上面介绍了 socket 中 TCP 的三次握手建立过程,及其涉及的 socket 函数。现在我们介绍 socket 中的四次握手释放连接的过程,请看下图:
图示过程如下:
这样每个方向上都有一个 FIN 和 ACK。
各大设计模式例子参考:CSDN专栏 . C++ 设计模式 系列博文
一般应用程序内存空间有如下区域:
栈保存了一个函数调用所需要的维护信息,常被称为堆栈帧(Stack Frame)或活动记录(Activate Record),一般包含以下几方面:
堆分配算法:
典型的非法指针解引用造成的错误。当指针指向一个不允许读写的内存地址,而程序却试图利用指针来读或写该地址时,会出现这个错误。
普遍原因:
平台 | 可执行文件 | 目标文件 | 动态库/共享对象 | 静态库 |
---|---|---|---|---|
Windows | exe | obj | dll | lib |
Unix/Linux | ELF、out | o | so | a |
Mac | Mach-O | o | dylib、tbd、framework | a、framework |
#include
、#define
等预编译指令,生成 .i
或 .ii
文件).s
文件).o
文件).out
文件)现在版本 GCC 把预编译和编译合成一步,预编译编译程序 cc1、汇编器 as、连接器 ld
MSVC 编译环境,编译器 cl、连接器 link、可执行文件查看器 dumpbin
编译器编译源代码后生成的文件叫做目标文件。目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。
可执行文件(Windows 的
.exe
和 Linux 的ELF
)、动态链接库(Windows 的.dll
和 Linux 的.so
)、静态链接库(Windows 的.lib
和 Linux 的.a
)都是按照可执行文件格式存储(Windows 按照 PE-COFF,Linux 按照 ELF)
.obj
格式.o
格式a.out
格式.COM
格式PE 和 ELF 都是 COFF(Common File Format)的变种
段 | 功能 |
---|---|
File Header | 文件头,描述整个文件的文件属性(包括文件是否可执行、是静态链接或动态连接及入口地址、目标硬件、目标操作系统等) |
.text section | 代码段,执行语句编译成的机器代码 |
.data section | 数据段,已初始化的全局变量和局部静态变量 |
.bss section | BSS 段(Block Started by Symbol),未初始化的全局变量和局部静态变量(因为默认值为 0,所以只是在此预留位置,不占空间) |
.rodata section | 只读数据段,存放只读数据,一般是程序里面的只读变量(如 const 修饰的变量)和字符串常量 |
.comment section | 注释信息段,存放编译器版本信息 |
.note.GNU-stack section | 堆栈提示段 |
其他段略
在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。
如下符号表(Symbol Table):
Symbol(符号名) | Symbol Value (地址) |
---|---|
main | 0x100 |
Add | 0x123 |
… | … |
Linux 下的共享库就是普通的 ELF 共享对象。
共享库版本更新应该保证二进制接口 ABI(Application Binary Interface)的兼容
libname.so.x.y.z
大部分包括 Linux 在内的开源系统遵循 FHS(File Hierarchy Standard)的标准,这标准规定了系统文件如何存放,包括各个目录结构、组织和作用。
/lib
:存放系统最关键和最基础的共享库,如动态链接器、C 语言运行库、数学库等/usr/lib
:存放非系统运行时所需要的关键性的库,主要是开发库/usr/local/lib
:存放跟操作系统本身并不十分相关的库,主要是一些第三方应用程序的库动态链接器会在
/lib
、/usr/lib
和由/etc/ld.so.conf
配置文件指定的,目录中查找共享库
LD_LIBRARY_PATH
:临时改变某个应用程序的共享库查找路径,而不会影响其他应用程序LD_PRELOAD
:指定预先装载的一些共享库甚至是目标文件LD_DEBUG
:打开动态链接器的调试功能创建一个名为 MySharedLib 的共享库
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.10) |
library.h
1 |
|
library.cpp
1 |
|
创建一个名为 TestSharedLib 的可执行项目
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.10) |
main.cpp
1 |
|
执行结果
1 | Hello, World! |
一个程序的 I/O 指代程序与外界的交互,包括文件、管程、网络、命令行、信号等。更广义地讲,I/O 指代操作系统理解为 “文件” 的事物。
_start -> __libc_start_main -> exit -> _exit
其中 main(argc, argv, __environ)
函数在 __libc_start_main
里执行。
int mainCRTStartup(void)
执行如下操作:
大致包含如下功能:
包含:
简而言之,回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。
因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为 int )的被调用函数。
如果想知道回调函数在实际中有什么作用,先假设有这样一种情况,我们要编写一个库,它提供了某些排序算法的实现,如冒泡排序、快速排序、 shell 排序、 shake 排序等等,但为使库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,想让库可用于多种数据类型( int 、 float 、 string ),此时,该怎么办呢?可以使用函数指针,并进行回调。
回调可用于通知机制,例如,有时要在程序中设置一个计时器,每到一定时间,程序会得到相应的通知,但通知机制的实现者对我们的程序一无所知。而此时,就需有一个特定原型的函数指针,用这个指针来进行回调,来通知我们的程序事件已经发生。实际上,SetTimer() API 使用了一个回调函数来通知计时器,而且,万一没有提供回调函数,它还会把一个消息发往程序的消息队列。
另一个使用回调机制的 API 函数是 EnumWindow() ,它枚举屏幕上所有的顶层窗口,为每个窗口调用一个程序提供的函数,并传递窗口的处理程序。如果被调用者返回一个值,就继续进行迭代,否则,退出。 EnumWindow() 并不关心被调用者在何处,也不关心被调用者用它传递的处理程序做了什么,它只关心返回值,因为基于返回值,它将继续执行或退出。
不管怎么说,回调函数是继续自 C 语言的,因而,在 C++ 中,应只在与 C 代码建立接口,或与已有的回调接口打交道时,才使用回调函数。除了上述情况,在 C++ 中应使用虚拟方法或函数符( functor ),而不是回调函数。
也可以这样,更容易理解:回调函数就好像是一个中断处理函数,系统在符合你设定的条件时自动调用。
为此,你需要做三件事:
声明;
定义;
设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用。
声明和定义时应注意:回调函数由系统调用,所以可以认为它属于WINDOWS系统,不要把它当作你的某个类的成员函数
回调函数是一个程序员不能显式调用的函数;通过将回调函数的地址传给调用者从而实现调用。回调函数使用是必要的,在我们想通过一个统一接口实现不同的内容,这时用回掉函数非常合适。比如,我们为几个不同的设备分别写了不同的显示函数:void TVshow(); void ComputerShow(); void NoteBookShow()…等等。这是我们想用一个统一的显示函数,我们这时就可以用回掉函数了。void show(void (*ptr)()); 使用时根据所传入的参数不同而调用不同的回调函数。
C语言中的回调函数
一文搞懂C语言回调函数
c语言实现回调函数
函数指针
钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。对每种类型的钩子由系统来维护一个钩子链,最近安装的钩子放在链的开始,而最先安装的钩子放在最后,也就是后加入的先获得控制权。
钩子函数是Windows消息处理机制的一部分,通过设置“钩子”,应用程序可以在系统级对所有消息、事件进行过滤,访问在正常情况下无法访问的消息。钩子的本质是一段用以处理系统消息的程序,通过系统调用,把它挂入系统。
局部钩子:仅钩挂您自己进程的事件。
远程钩子:可以钩挂自己进程或其他进程的事件,
远程钩子又分为两种:
全局钩子函数需要定义在 DLL 中,线程级的钩子中经常用到 GetCurrentThreadID 函数来获取当前线程的ID。
当创建一个钩子时,WINDOWS会先在内存中创建一个数据结构,该数据结构包含了钩子的相关信息,然后把该结构体加到已经存在的钩子链表中去。新的钩子将加到老的前面。当一个事件发生时,如果安装的是一个局部钩子,自己进程中的钩子函数将被调用。如果是一个远程钩子,系统就必须把钩子函数插入到其他进程的地址空间,要做到这一点要求钩子函数必须在一个动态链接库中,所以如果想要使用远程钩子,就必须把该钩子函数放到动态链接库中去。
两个例外:
这两个钩子的钩子函数必须在安装钩子的线程中。原因是:
解决办法:把钩子函数放到单个的线程中,譬如安装钩子的线程。
浅谈c++ hook 钩子的使用介绍
异步消息的传递-回调机制
软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:
同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。
回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。
同步调用是三者当中最简单的,而回调又常常是异步调用的基础,因此,下面我们着重讨论回调机制在不同软件架构中的实现。
对于不同类型的语言(如结构化语言和对象语言)、平台(Win32、JDK)或构架(CORBA、DCOM、WebService),客户和服务的交互除了同步方式以外,都需要具备一定的异步通知机制,让服务方(或接口提供方)在某些情况下能够主动通知客户,而回调是实现异步的一个最简捷的途径。
对于一般的结构化语言,可以通过回调函数来实现回调。回调函数也是一个函数或过程,不过它是一个由调用方自己实现,供被调用方使用的特殊函数。
在面向对象的语言中,回调则是通过接口或抽象类来实现的,我们把实现这种接口的类成为回调类,回调类的对象成为回调对象。对于象C++或Object Pascal这些兼容了过程特性的对象语言,不仅提供了回调对象、回调方法等特性,也能兼容过程语言的回调函数机制。
Windows平台的消息机制也可以看作是回调的一种应用,我们通过系统提供的接口注册消息处理函数(即回调函数),从而实现接收、处理消息的目的。由于Windows平台的API是用C语言来构建的,我们可以认为它也是回调函数的一个特例。
对于分布式组件代理体系CORBA,异步处理有多种方式,如回调、事件服务、通知服务等。事件服务和通知服务是CORBA用来处理异步消息的标准服务,他们主要负责消息的处理、派发、维护等工作。对一些简单的异步处理过程,我们可以通过回调机制来实现。
下面我们集中比较具有代表性的语言(C、Object Pascal)和架构(CORBA)来分析回调的实现方式、具体作用等。
回调在C语言中是通过函数指针来实现的,通过将回调函数的地址传给被调函数从而实现回调。因此,要实现回调,必须首先定义函数指针,请看下面的例子:
1 | void Func(char *s);// 函数原型 |
可以看出,函数的定义和函数指针的定义非常类似。
一般的话,为了简化函数指针类型的变量定义,提高程序的可读性,我们需要把函数指针类型自定义一下。
1 | typedef void(*pcb)(char *); |
回调函数可以象普通函数一样被程序调用,但是只有它被当作参数传递给被调函数时才能称作回调函数。
被调函数的例子:
1 | void GetCallBack(pcb callback) |
如果赋了不同的值给该参数,那么调用者将调用不同地址的函数。赋值可以发生在运行时,这样使你能实现动态绑定。
到目前为止,我们只讨论了函数指针及回调而没有去注意 ANSI C/C++ 的编译器规范。许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加 _cdecl
,_stdcall
或者 _pascal
来表示其调用规范(默认为 _cdecl
)。C++ Builder也支持 _fastcall
调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。
将调用规范看成是函数类型的一部分是很重要的;不能用不兼容的调用规范将地址赋值给函数指针。例如:
1 | // 被调用函数是以 int 为参数,以 int 为返回值 |
指针 p 和 callee() 的类型不兼容,因为它们有不同的调用规范。因此不能将被调用者的地址赋值给指针p,尽管两者有相同的返回值和参数列
C 语言的标准库函数中很多地方就采用了回调函数来让用户定制处理过程。如常用的快速排序函数、二分搜索函数等。
1 | // 快速排序函数原型: |
其中 fcmp 就是一个回调函数的变量。
下面给出一个具体的例子:
1 |
|
CORBA 的消息传递机制有很多种,比如回调接口、事件服务和通知服务等。回调接口的原理很简单,CORBA 客户和服务器都具有双重角色,即充当服务器也是客户客户。
回调接口的反向调用与正向调用往往是同时进行的,如果服务端多次调用该回调接口,那么这个回调接口就变成异步接口了。因此,回调接口在 CORBA 中常常充当事件注册的用途,客户端调用该注册函数时,客户函数就是回调函数,在此后的调用中,由于不需要客户端的主动参与,该函数就是实现了一种异步机制。
从 CORBA 规范我们知道,一个 CORBA 接口在服务端和客户端有不同的表现形式,在客户端一般使用桩(Stub)文件,服务端则用到框架(Skeleton)文件,接口的规格采用 IDL 来定义。而回调函数的引入,使得服务端和客户端都需要实现一定的桩和框架。下面是回调接口的实现模型:
下面给出了一个使用回调的接口文件,服务端需要实现 Server 接口的框架,客户端需要实现 CallBack 的框架:
1 | module cb |
客户端首先通过同步方式调用服务端的接口 RegistCB,用来注册回调接口 CallBack。服务端收到该请求以后,就会保留该接口引用,如果发生某种事件需要向客户端通知的时候就通过该引用调用客户方的 OnEvent 函数,以便对方及时处理。
为了防止野指针带来的灾难,建议指针在定义时给一个初值,比如“NULL”,意思是不指向任何内存地址。然后再使用malloc函数给指针分配一块存储空间。
1 |
|
基本知识
1、进程和线程
2、多线程通讯方式
3、消费者和生产者模式(消费者是否轮询方式读取消息,用等待信号方式)
4、linux命令 top、netstat
5、gdb调试,怎样切换到某个线程
6、inline和宏定义区别
7、vector和list区别,什么情况分别是用什么
8、类的什么函数不能作为虚函数、析构函数能否作为虚函数,虚函数怎么实现的
9、setsocektopt no-delay,等参数的作用
10、tcp关闭时的几个步骤,tcp的慢启动时啥意思,,,,
11、epoll模型,我说的是多线程,每个线程一个epoll,一个专门接收链接,另外的读数据 ,解码在哪个线程中进行
12、c++11 智能指针
13、死锁概念
14、什么叫做稳定排序、有哪些排序算法、快排怎么实现的
15、怎么样判断一棵树和平衡二叉树
16、当前编写代码(输入一个字符串和一个分隔符,,,,,,把字符串用分割符分割几部分,然后输出)
项目:
1、freeswitch的系统结构模型、并发的语音的最大路数,语音编码
2、视频花屏是怎样优化的
3、rtp,udp
4、语音包、和视频包是不是固定大小的,,,是否分包
]]>md5
加密存储的。目前需要对员工的年龄、学历、工作年限等进行排序,如果只有几十个上百个样本,应该不会那么麻烦;关键这是几万名员工的数据,这个量很大,马虎不得。悄悄的告诉你,别惹我,我懂得删库跑路哦。脑海中对排序的记忆有点模糊,只对「归并排序」印象较为深刻,为了加深理解,重拾「数据结构与算法」,并总结了一下常用的十大经典排序算法,由于平台为linux
,因此代码全部用C++
实现,全部源码均在linux
下编译通过并测试成功,可以作为参考。
排序算法在程序猿的编程生涯中虽然用的不多,但是作为基本功,还是要掌握一下。排序算法是「数据结构与算法」中最基本的算法,它分为「内部排序」和「外部排序」;「内部排序」一般在内存中实现;当数据量很大时,内存有限,不能将所有的数据都放到内存中来,这个时候必须使用「外部排序」。
先看一张图,对常用算法的时间复杂度做个比较:
排序算法 | 平均时间复杂度 | 最佳情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | In-place | 稳定 |
选择排序 | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(1)$ | In-place | 不稳定 |
插入排序 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | In-place | 稳定 |
希尔排序 | $O(n \log n)$ | $O(n \log^2 n)$ | $O(n \log^2 n)$ | $O(1)$ | In-place | 不稳定 |
归并排序 | $O(n \log n)$ | $O(n \log n)$ | $O(n \log n)$ | $O(n)$ | Out-place | 稳定 |
快速排序 | $O(n \log n)$ | $O(n \log n)$ | $O(n^2)$ | $O(\log n)$ | In-place | 不稳定 |
堆排序 | $O(n \log n)$ | $O(n \log n)$ | $O(n \log n)$ | $O(1)$ | In-place | 不稳定 |
计数排序 | $O(n+k)$ | $O(n+k)$ | $O(n+k)$ | $O(k)$ | Out-place | 稳定 |
桶排序 | $O(n+k)$ | $O(n+k)$ | $O(n^2)$ | $O(n+k)$ | Out-place | 稳定 |
基数排序 | $O(n \times k)$ | $O(n \times k)$ | $O(n \times k)$ | $O(n+k)$ | Out-place | 稳定 |
这里的「稳定」是指当排序后两个相等键值的顺序和排序之前的顺序相同;
冒泡排序是排序算法中较为简单的一种,英文称为Bubble Sort。
它遍历所有的数据,每次对相邻元素进行两两比较,如果顺序和预先规定的顺序不一致,则进行位置交换;这样一次遍历会将最大或最小的数据上浮到顶端,之后再重复同样的操作,直到所有的数据有序。
如果有$n$个数据,那么需要$O(n^2)$的比较次数,所以当数据量很大时,冒泡算法的效率并不高。
当输入的数据是反序时,花的时间最长,当输入的数据是正序时,时间最短。
平均时间复杂度:$O(n^2)$
空间复杂度:$O(1)$
动态演示:
1 |
|
新建代码文件bubble_sort.cpp,
将以上代码写入,linux
下编译:
1 | g++ -o bubble_sort bubble_sort.cpp |
测试:
1 | ./bubble_sort |
输出结果:
1 | 1 17 21 22 29 34 50 60 61 62 72 |
以下的编译方法和测试方法和这里一样,所以下面不再重复编译和测试的说明。
选择排序简单直观,英文称为Selection Sort,
先在数据中找出最大或最小的元素,放到序列的起始;然后再从余下的数据中继续寻找最大或最小的元素,依次放到排序序列中,直到所有数据样本排序完成。很显然,选择排序也是一个费时的排序算法,无论什么数据,都需要$O(n^2)$的时间复杂度,不适宜大量数据的排序。
平均时间复杂度::$O(n^2)$
空间复杂度::$O(1)$
动态演示:
1 |
|
插入排序英文称为Insertion Sort,
它通过构建有序序列,对于未排序的数据序列,在已排序序列中从后向前扫描,找到相应的位置并插入,类似打扑克牌时的码牌。插入排序有一种优化的算法,可以进行拆半插入。
基本思路是先将待排序序列的第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列;然后从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置,直到所有数据都完成排序;如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。
平均时间复杂度::$O(n^2)$
空间复杂度::$O(1)$
动态演示:
1 |
|
希尔排序也称递减增量排序,是插入排序的一种改进版本,英文称为Shell Sort
,效率虽高,但它是一种不稳定的排序算法。
插入排序在对几乎已经排好序的数据操作时,效果是非常好的;但是插入排序每次只能移动一位数据,因此插入排序效率比较低。
希尔排序在插入排序的基础上进行了改进,它的基本思路是先将整个数据序列分割成若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时,再对全部数据进行依次直接插入排序。
平均时间复杂度::$O(n \log n)$
空间复杂度::$O(1)$
假如有这样一组数据,[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果以步长5
进行分割,每一列为一组,那么这组数据应该首先分成这样
1 | 13 14 94 33 82 |
之后对每列进行插入排序:
1 | 10 14 73 25 23 |
将上述四行数据依序拼接在一起,得到[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ],此时10
已经移到正确的顺序了,之后以步长3
进行插入排序:
1 | 10 14 73 |
排序之后变为:
1 | 10 14 13 |
最后以步长 1 进行排序。
步长的选择是希尔排序的关键,只要最终步长为1
,任何步长序列都可以。建议最初步长选择为数据长度的一半,直到最终的步长为1
。
图解:
1 |
|
归并排序英文称为Merge Sort
,归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)
的一个非常典型的应用。它首先将数据样本拆分为两个子数据样本, 并分别对它们排序, 最后再将两个子数据样本合并在一起; 拆分后的两个子数据样本序列, 再继续递归的拆分为更小的子数据样本序列, 再分别进行排序, 直到最后数据序列为1,而不再拆分,此时即完成对数据样本的最终排序。
归并排序严格遵循从左到右或从右到左的顺序合并子数据序列, 它不会改变相同数据之间的相对顺序, 因此归并排序是一种稳定的排序算法.
作为一种典型的分而治之思想的算法应用,归并排序的实现分为两种方法:
平均时间复杂度::$O(n \log n)$
空间复杂度::$O(n)$
动态演示:
1 |
|
快速排序,英文称为Quicksort,又称划分交换排序 partition-exchange sort 简称快排。
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。首先从数列中挑出一个元素,并将这个元素称为「基准」,英文pivot。重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。之后,在子序列中继续重复这个方法,直到最后整个数据序列排序完成。
在平均状况下,排序n个项目要$O(n \log n)$次比较。在最坏状况下则需要$O(n^2)$次比较,但这种状况并不常见。事实上,快速排序通常明显比其他算法更快,因为它的内部循环可以在大部分的架构上很有效率地达成。
平均时间复杂度:: $O(n \log n)$
空间复杂度: :$O(\log n)$
动态演示:
更直观一些的动图演示:
代码分两种方式实现,分别为迭代法和递归法。
1 | struct Range { |
1 | template <typename T> |
堆排序,英文称Heapsort,是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序实现分为两种方法:
算法步骤:
平均时间复杂度: :$O(n \log n)$
空间复杂度: :$O(1)$
动图演示:
来一个更直观一些的:
1 |
|
计数排序英文称Counting sort,是一种稳定的线性时间排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于 i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。基本的步骤如下:
平均时间复杂度::$O(n + k )$
空间复杂度: :$O(k)$
动图演示:
1 | void Count_Sort(int* Data, int Len) |
桶排序也称为箱排序,英文称为 Bucket Sort。它是将数组划分到一定数量的有序的桶里,然后再对每个桶中的数据进行排序,最后再将各个桶里的数据有序的合并到一起。
平均时间复杂度::$O(n + k)$
空间复杂度::$O(n + k)$
动态演示:
1 |
|
基数排序英文称Radix sort,是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串和特定格式的浮点数,所以基数排序也仅限于整数。它首先将所有待比较数值,统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
平均时间复杂度: :$O(n \times k)$
空间复杂度: :$O(n + k )$
动态演示:
1 | int maxbit(int data[], int n) //辅助函数,求数据的最大位数 |
wiki
https://github.com/hustcc/JS-Sorting-Algorithm
「数据结构与算法」
「算法导论」
疑惑
1、我学完了C语言,可是现在感觉还是写不出代码。
2、为什么会有各种各样的程序存在?
3、程序的本质是什么?
程序是为了具体问题而存在的
程序需要围绕问题的解决进行设计
同一个问题可以有多种解决方案
如何追求程序的“性价比”?
是否有可量化的方法判别程序的好坏?
数据结构起源
计算机从解决数值计算问题到解决生活中的问题
现实生活中的问题涉及不同个体间的复杂联系
需要在计算机程序中描述生活中个体间的联系
*数据结构主要研究非数值计算程序问题中的操作对象以及它们之间的关系 *
不是研究复杂的算法
数据结构中的基本概念
数据 – 程序的操作对象,用于描述客观事物 (int a, int b,)
数据的特点:
可以输入到计算机
可以被计算机程序处理
数据是一个抽象的概念,将其进行分类后得到程序设计语言中的类型。如:int,float,char等等
数据元素:组成数据的基本单位
数据项:一个数据元素由若干数据项组成
数据对象 – 性质相同的数据元素的集合 (比如:数组,链表)
1 | //声明一个结构体类型 |
数据元素之间不是独立的,存在特定的关系,这些关系即结构
*数据结构指数据对象中数据元素之间的关系 *
如:数组中各个元素之间存在固定的线性关系
基本概念总结:
数据的逻辑结构
指数据元素之间的逻辑关系。即从逻辑关系上描述数据,它与数据的存储无关,是独立于计算机的。逻辑结构可细分为4类:
数据的物理结构
数据的运算
算法概念
算法是特定问题求解步骤的描述
在计算机中表现为指令的有限序列
算法是独立存在的一种解决问题的方法和思想。
对于算法而言,语言并不重要,重要的是思想。
算法和数据结构区别
数据结构只是静态的描述了数据元素之间的关系
高效的程序需要在数据结构的基础上设计和选择算法
程序 = 数据结构 + 算法
总结:
算法是为了解决实际问题而设计的
数据结构是算法需要处理的问题载体
数据结构与算法相辅相成
算法特性
输入
输出
有穷性
确定性
可行性
算法效率的度量
1、事后统计法
算法效率的度量
1 | //算法最终编译成具体的计算机指令 |
注意 1:判断一个算法的效率时,往往只需要关注操作数量的最高次项,其它次要项和常数项可以忽略。
注意 2:在没有特殊说明时,我们所分析的算法的时间复杂度都是指最坏时间复杂度。
2、大 O 表示法
算法效率严重依赖于操作(Operation)数量
在判断时首先关注操作数量的最高次项
操作数量的估算可以作为时间复杂度的估算
1 | O(5) = O(1) |
常见时间复杂度
关系
3、算法的空间复杂度
算法的空间复杂度通过计算算法的存储空间实现
1 | S(n) = O(f(n)) |
其中,n
为问题规模,f(n)
为在问题规模为 n
时所占用存储空间的函数
大 O 表示法同样适用于算法的空间复杂度
当算法执行时所需要的空间是常数时,空间复杂度为O(1)
空间与时间的策略:
多数情况下,算法执行时所用的时间更令人关注
如果有必要,可以通过增加空间复杂度来降低时间复杂度
同理,也可以通过增加时间复杂度来降低空间复杂度
练习1:分析 sum1 sum2 sum3 函数的空间复杂度
1 | O(4n+12) O(8)=O(1) O(4)=O(1) |
总结:实现算法时,需要分析具体问题,对执行时间和空间的要求。
练习2:时间换空间
1 | /* |
把每个数字出现的次数的中间结果,缓存下来;在缓存的结果中求最大值。
线性表定义
线性表(List)是零个或多个数据元素的集合
线性表中的数据元素之间是有顺序的
线性表中的数据元素个数是有限的
线性表中的数据元素的类型必须相同
数学定义
线性表是具有相同类型的 n( ≥ 0)个数据元素的有限序列
1 | (a1, a2, …, an) |
性质
a0 为线性表的第一个元素,只有一个后继
an 为线性表的最后一个元素,只有一个前驱
除 a0 和 an 外的其它元素 ai,既有前驱,又有后继
线性表能够逐项访问和顺序存取
练习
下面的关系中可以用线性表描述的是
A.班级中同学的友谊关系 N:N
B.公司中的上下级关系 1:N
C.冬天图书馆排队占座关系
D.花名册上名字之间的关系 1::1
线性表的操作
创建线性表
销毁线性表
清空线性表
将元素插入线性表
将元素从线性表中删除
获取线性表中某个位置的元素
获取线性表的长度
线性表在程序中表现为一种特殊的数据类型
线性表的操作在程序中的表现为一组函数
1 | /* |
基本概念
设计与实现
插入元素算法
判断线性表是否合法
判断插入位置是否合法
把最后一个元素到插入位置的元素后移一个位置
将新元素插入
线性表长度加 1
获取元素操作
判断线性表是否合法
判断位置是否合法
直接通过数组下标的方式获取元素
删除元素算法
判断线性表是否合法
判断删除位置是否合法
将元素取出
将删除位置后的元素分别向前移动一个位置
线性表长度减 1
链表顺序存储插入算法和删除算法
优点和缺点
优点:
无需为线性表中的逻辑关系增加额外的空间
可以快速的获取表中合法位置的元素
缺点:
插入和删除操作需要移动大量元素
当线性表长度变化较大时难以确定存储空间的容量
基本概念
链式存储定义
表头结点
数据结点
尾结点
链表技术领域推演
设计与实现
链表链式存储 _api
实现分析
在C语言中可以用结构体来定义链表中的指针域
链表中的表头结点也可以用结构体实现
1 | typedef void * LK; // 不希望看到内部数据是可以这么定义 |
1 | // 带头结点、位置从0的单链表 |
返回第三个位置的,移动pos次以后,当前指针指向哪里?
答案:指向位置2,所以需要返回 ret = current->next;
1 | /* |
所以如果想返回位置 n 的元素的值,需要怎么做 ret = current->next;
此问题是:*指向头结点的指针移动 n 次 和 第 n 个元素之间的关系? *
删除元素
优点和缺点
优点:
无需一次性定制链表的容量
插入和删除操作无需移动数据元素
缺点:
数据元素必须保存后继元素的位置信息
获取指定数据的元素操作需要顺序访问之前的元素
基本概念
循环链表的定义:将单链表中最后一个数据元素的next指针指向第一个元素
循环链表拥有单链表的所有操作
创建链表
销毁链表
获取链表长度
清空链表
获取第pos个元素操作
插入元素到位置pos
删除位置pos处的元素
新增功能:游标 的定义
在循环链表中可以定义一个“当前”指针,这个指针通常称为 游标,可以通过这个游标来遍历链表中的所有元素。
循环链表新操作
1 | // 将游标重置指向链表中的第一个数据元素 |
循环链表的应用
证明循环链表
约瑟夫问题求解
约瑟夫问题 - 循环链表典型应用
n 个人围成一个圆圈,首先第 1 个人从 1 开始一个人一个人顺时针报数,报到第 m 个人,令其出列。然后再从下一 个人开始从 1 顺时针报数,报到第 m 个人,再令其出列,…,如此下去,求出列顺序。
设计与实现
循环链表插入元素的分析
1) 普通插入元素(和单链表是一样的)
2) 尾插法(和单链表是一样的,单链表的写法支持尾插法;因:辅助指针向后跳length次,指向最后面那个元素)
1 | void CircleList_Insert(list, (CircleListNode*)&v1, CircleList_Length(list)); |
3) 头插法(要进行头插法,需要求出尾结点,和单链表不一样的地方,保证是循环链表)第一次插入元素时,让游标指向 0 号结点
1 | void CircleList_Insert(list, (CircleListNode*)&v1, 0); |
4)第一次插入元素
循环链表插入综合场景分析图
循环链表删除结点分析
1、 删除普通结点
2、 删除头结点(删除 0 号位置处元素),需要求出尾结点
优点和缺点
优点:功能强了。
循环链表只是在单链表的基础上做了一个加强
循环链表可以完全取代单链表的使用
循环链表的 Next 和 Current 操作可以高效的遍历链表中的所有元素
缺点:
基本概念
请思考: 为什么 需要 双向链表?
单链表的结点都只有一个指向下一个结点的指针
单链表的数据元素无法直接访问其前驱元素
逆序访问单链表 中的元素是极其 耗时 的操作!
1 | len = LinkList_Length(list); |
双向链表的定义
在单链表的结点中增加一个指向其前驱的 pre 指针
双向链表拥有单链表的所有操作
创建链表
销毁链表
获取链表长度
清空链表
获取第 pos 个元素操作
插入元素到位置 pos
删除位置 pos 处的元素
设计与实现
循环链表一般操作
插入操作
插入操作异常处理
插入第一个元素异常处理
在 0 号位置处插入元素;
删除操作
删除操作异常处理
双向链表的新操作
获取当前游标指向的数据元素
将游标重置指向链表中的第一个数据元素
将游标移动指向到链表中的下一个数据元素
将游标移动指向到链表中的上一个数据元素
直接指定删除链表中的某个数据元素
1 | DLinkListNode* DLinkList_DeleteNode(DLinkList* list, DLinkListNode* node); |
优点和缺点
优点:
双向链表在单链表的基础上增加了指向前驱的指针
功能上双向链表可以完全取代单链表的使用
双向链表的 Next,Pre 和 Current 操作可以高效的遍历链表中的所有元素
缺点:
Stack基本概念
栈是一种 特殊的线性表
栈仅能在线性表的一端进行操作
Stack的常用操作
创建栈
销毁栈
清空栈
进栈
出栈
获取栈顶元素
获取栈的大小
1 |
|
栈模型和链表模型关系分析
栈的顺序存储设计与实现
设计与实现
1 |
|
栈的链式存储设计与实现
设计与实现
1 |
|
栈的应用
案例1:就近匹配
应用1:就近匹配
几乎所有的编译器都具有检测括号是否匹配的能力
如何实现编译器中的符号成对检测?
1 |
|
算法思路
从第一个字符开始扫描
当遇见普通字符时忽略,
当遇见左符号时压入栈中
当遇见右符号时从栈中弹出栈顶符号,并进行匹配
- 匹配成功:继续读入下一个字符
- 匹配失败:立即停止,并报错
结束:
- 成功: 所有字符扫描完毕,且栈为空
- 失败:匹配失败或所有字符扫描完毕但栈非空
当需要检测成对出现但又互不相邻的事物时,可以使用栈 “后进先出” 的特性,栈非常适合于需要“就近匹配”的场合
案例2:中缀表达式和后缀表达式
应用2:中缀 后缀
计算机的本质工作就是做数学运算,那计算机可以读入字符串
“9 + (3 - 1) * 5 + 8 / 2”并计算值吗?
后缀表达式 ==?符合计算机运算
波兰科学家在20世纪50年代提出了一种将运算符放在数字后面的后缀表达式对应的,
我们习惯的数学表达式叫做中缀表达式===》符合人类思考习惯
1 | // 实例: |
中缀表达式符合人类的阅读和思维习惯
后缀表达式符合计算机的“运算习惯”
如何将中缀表达式转换成后缀表达式?
中缀转后缀算法:
遍历中缀表达式中的数字和符号
对于数字:直接输出
对于符号:
- 左括号:进栈
- 运算符号:与栈顶符号进行优先级比较
- 若栈顶符号优先级低:此符合进栈 (默认栈顶若是左括号,左括号优先级最低)
- 若栈顶符号优先级不低:将栈顶符号弹出并输出,之后进栈
右括号:将栈顶符号弹出并输出,直到匹配左括号
遍历结束:将栈中的所有符号弹出并输出
中缀转后缀
计算机是如何基于后缀表达式计算的?
8 3 1 – 5 * +
遍历后缀表达式中的数字和符号
对于数字:进栈
对于符号:
从栈中弹出右操作数
从栈中弹出左操作数
根据符号进行运算
将运算结果压入栈中
遍历结束:栈中的唯一数字为计算结果
栈的神奇!
中缀表达式是人习惯的表达方式
后缀表达式是计算机喜欢的表达方式
通过栈可以方便的将中缀形式变换为后缀形式
中缀表达式的计算过程类似程序编译运行的过程
扩展:给你一个字符串,计算结果
“1 + 2 * (66 / (2 * 3) + 7 )”
字符串解析
词法语法分析
优先级分析
数据结构选型===》栈还是树?
queue基本概念
队列是一种特殊的线性表
队列仅在线性表的两端进行操作
队头(Front):取出数据元素的一端
队尾(Rear):插入数据元素的一端
队列不允许在中间部位进行操作!
queue常用操作
销毁队列
清空队列
进队列
出队列
获取队头元素
获取队列的长度
1 |
|
队列模型和链表模型关系分析
队列的顺序存储设计与实现
队列也是一种特殊的线性表;可以用线性表顺序存储来模拟队列。
设计与实现
1 |
|
队列的链式存储设计与实现
队列也是一种特殊的线性表;可以用线性表链式存储来模拟队列的链式存储。
设计与实现
1 |
|
树基本概念
非线性结构,一个直接前驱,但可能有多个直接后继(1:n)
树的表示法
图形表示法
广义表表示法
左孩子-右兄弟表示法
双亲孩子表示法
树的逻辑结构
一对多(1:n),有多个直接后继(如家谱树、目录树等等),但只有一个根结点,且子树之间互不相交。
广义表表示法
左孩子-右兄弟表示法
先序遍历(DLR):先访问根、再访问左、再访问右
中序遍历(LDR):先访问左、再访问根、再访问右
后序遍历(LRD):先访问左、再访问右、再访问根
二叉树的结构最简单,规律性最强。可以证明,所有树都能转为唯一对应的二叉树,不失一般性
定义:是 n(n≥0)个结点的有限集合,由一个根结点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成
二叉树性质
性质1: 在二叉树的第
i
层上至多有 个结点(i>0
)
性质2: 深度为
k
的二叉树至多有 个结点(k>0
)
性质3: 对于任何一棵二叉树,若 2 度的结点数有 个,则叶子数()必定为 (即)
满二叉树:一棵深度为 k
且有 个结点的二叉树。(特点:每层都“充满”了结点)
完全二叉树:深度为 k
的,有 n
个结点的二叉树,当且仅当其每一个结点都与深度为 k
的满二叉树中编号从 1 至 n
的结点一一对应。
理解:(k-1
层与满二叉树完全相同,第 k
层结点尽力靠左)
性质4: 具有
n
个结点的完全二叉树的深度必为
性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为
i
的结点,其左孩子编号必为2i
,其右孩子编号必为2i + 1
;其双亲的编号必为i/2
(i=1
时为根,除外)
二叉树的存储结构
1、顺序存储结构
按二叉树的结点“自上而下、从左至右”编号,用一组连续的存储单元存储。
答:一律转为完全二叉树!
讨论:不是完全二叉树怎么办?
方法很简单,将各层空缺处统统补上“虚结点”,其内容为空
2、链式存储结构
二叉树的表示
1 | /* |
树的三叉链表表示
1 | typedef struct TriTNode |
二叉树的遍历
树的遍历本质剖析
1 | typedef struct node{ |
先序遍历算法
1 | DLR(NODE *root ) |
中序遍历算法
1 | LDR(NODE *root) |
后序遍历算法
1 | LRD (NODE *root) |
案例1:计算二叉树中叶子结点的数目
1 | int sum = 0; //全局变量 |
思想: 1)求根结点左子树的叶子结点个数,累计到sum中,求根结点右子树的叶子结点个数累计到sum中。
2)若左子树还是树,重复步骤1;若右子树还是树,重复步骤1。
3)全局变量转成函数参数
4)按照先序、中序、后序方式计算叶子结点,
===》三种遍历的本质思想强化:访问结点的路径都是一样的,计算结点的时机不同。
案例2:求二叉树的深度
思想: 1)求根结点左子树高度,根结点右子树高度,比较的子树最大高度,再 +1。
2)若左子树还是树,重复步骤 1;若右子树还是树,重复步骤 1。
案例3:完全Copy二叉树
思想: 1)malloc新结点,
2)拷贝左子树,拷贝右子树,让新结点连接左子树,右子树
3)若左子树还是树,重复步骤1、2;若右子树还是树,重复步骤1、2。
案例4:树的非递归遍历(中序遍历)
中序 遍历的几种情况
分析1:
- 什么时候访问根、什么时候访问左子树、什么访问右子树
- 当左子树为空或者左子树已经访问完毕以后,再访问根
- 访问完毕根以后,再访问右子树。
分析2:
- 非递归遍历树,访问结点时,为什么是栈,而不是其他模型(比如说是队列)。
- 先走到的后访问、后走到的先访问,显然是栈结构
分析3:结点所有路径情况
步骤1:
- 如果结点有左子树,该结点入栈;
- 如果结点没有左子树,访问该结点;
步骤2:
- 如果结点有右子树,重复步骤1;
- 如果结点没有右子树(结点访问完毕),根据栈顶指示回退,访问栈顶元素,并访问右子树,重复步骤1
- 如果栈为空,表示遍历结束。
注意:入栈的结点表示,本身没有被访问过,同时右子树也没有被访问过。
分析4:有一个一直往左走入栈的操作,中序遍历的起点
作业:自己编写堆栈函数原型,实现中序遍历非递归算法
中序和先序创建树
1、根据中序遍历的结果能确定一棵树吗?
中序遍历:结果为:“12345”,这个“12345”能确定一棵树吗?
请思考,会有多少种形状。
2、如何才能确定一棵树?
结论: 通过中序遍历和先序遍历可以确定一个树
通过中序遍历和后续遍历可以确定一个树
通过先序遍历和后序遍历确定不了一个树。
单独先序遍历:能求解根,但不能求解左子树什么时候结束、右子树什么时候开始。
3、根据先序和中序结果画树
算法
1、通过先序遍历找到根结点A,再通过A在中序遍历的位置找出左子树,右子树
2、在A的左子树中,找左子树的根结点(在先序中找),转步骤1
3、在A的右子树中,找右子树的根结点(在先序中找),转步骤1
讲解:
先序遍历结果:ADEBCF
中序遍历结果:DEACFB
练习:
先序遍历结果:ABDHKECFIGJ
中序遍历结果:HKDBEAIFCGJ
4、学习算法可借助工具、动画
#号法创建树
1、什么是 #
号法创建树
#
创建树,让树的每一个节点都变成度数为2的树
先序遍历:124###3##
可以唯一确定一棵树吗,为什么?
2、#
创建树练习
先序遍历:ABDH#K###E##CFI###G#J##
,请画出树的形状
#
号法画出树关键点:
要清楚的确定左子树什么结束,右子树什么时候开始。
3、#
号法编程实践
利用前序遍历来建树(结点值陆续从键盘输入,用 DLR 为宜)
1 | Bintree createBTpre( ) |
线索化概念
1、前言
普通二叉树只能找到结点的左右孩子信息,而该结点的直接前驱和直接后继只能在遍历过程中获得。
若可将遍历后对应的有关前驱和后继预存起来,则从第一个结点开始就能很快“顺藤摸瓜”而遍历整个树了。
二叉线索树思想是干什么的?
中序遍历这棵树===》转换成链表访问
2、线索化思想
结论: 线索化过程就是在遍历过程(假设是中序遍历)中修改空指针的过程:
将空的lchild改为结点的直接前驱;
将空的rchild改为结点的直接后继。
3、线索化思想训练
请将此树线索化。
1)右空指针线索化:
2)左空指针线索化
3)总结
线索化的实现
1)线索化树结点
1 | typedef struct BiThrNode/* 二叉线索存储结点结构 */ |
2)线索化思想分析
线索化的本质:让前后结点,建立关系;
1)两个辅助指针变量形成差值后:后继结点的左孩子指向前驱结点,前驱结点的右孩子指向后继结点。
2)赋值指针变量和业务操作的逻辑关系
4) 二叉树线索化树的遍历
1 | /* 中序遍历二叉线索树T(头结点)的非递归算法 */ |
组建一个网络,耗费最小 WPL最小;这个方法是霍夫曼想出来的,称为霍夫曼树
霍夫曼树的构造
对于文本 ”BADCADFEED” 的传输而言,因为重复出现的只有 ”ABCDEF” 这6个字符,因此可以用下面的方式编码:
接收方可以根据每3个bit进行一次字符解码的方式还原文本信息。
这样的编码方式需要30个bit位才能表示10个字符
那么当传输一篇500个字符的情报时,需要15000个bit位
在战争年代,这种编码方式对于情报的发送和接受是很低效且容易出错的。
如何提高收发效率?
要提高效率,必然要从编码方式的改进入手,要避免每个字符都占用相同的bit位
准则:任一字符的编码都不是另一个字符编码的前缀!
也就是说:每一个字符的编码路径,都不包含另外一个字符的路径。
霍夫曼树
1、给定 n 个数值 { v1, v2, …, vn}
2、根据这 n 个数值构造二叉树集合 F
F = { T1, T2, …, Tn}
Ti 的数据域为 vi,左右子树为空
3、在 F 中选取两棵根结点的值最小的树作为左右子树构造一棵新的二叉树,这棵二叉树的根结点中的值为左右子树根结点中的值之和
4、在 F 中删除这两棵子树,并将构造的新二叉树加入F中
5、重复 3 和 4,直到 F 中只剩下一个树为止。这棵树即霍夫曼树
假设经过统计 ABCDEF 在需要传输的报文中出现的概率如下
霍夫曼树是一种特殊的二叉树
霍夫曼树应用于信息编码和数据压缩领域
霍夫曼树是现代压缩算法的基础
排序是计算机内经常进行的一种操作,其目的是将一组“无序”的数据元素调整为“有序”的数据元素。
排序数学定义:
排序的稳定性:
多关键字排序:
排序时需要比较的关键字多余一个
排序结果首先按关键字1进行排序
当关键字1相同时按关键字2进行排序
当关键字n-1相同时按关键字n进行排序
对于多关键字排序,只需要在比较操作时同时考虑多个关键字即可!
排序中的关键操作:
比较
交换
内排序和外排序:
内排序
外排序
排序的审判:
时间性能
辅助存储空间
算法的实现复杂性
总结:
排序是数据元素从无序到有序的过程
排序具有稳定性,是选择排序算法的因素之一
比较和交换是排序的基本操作
多关键字排序与单关键字排序无本质区别
排序的时间性能是区分排序算法好坏的主要因素
基本思想:
排序过程:
首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记录交换
再通过n-2次比较,从剩余的n-1个记录中找出关键字次小的记录,将它与第二个记录交换
重复上述操作,共进行n-1趟排序后,排序结束
基本思想:
排序过程:
实质:对线性表执行 n-1 次插入操作,只是先要找到插入位置
V[0], V[1], …, V[i-1] 已经排好序。这时已经排好序。这时,用V[i]的关键字与 V[i-1], V[i-2], …的关键字进行比较, 找到插入位置即将V[i]]插入, 原来位置上的对象向后顺移。
插入排序关键点:
排序过程:
O(n-1.3)
Q(nlogn)
希尔排序是不稳定的。
思想:
快速排序是对冒泡排序的一种改进。它的基本思想是:
通过一躺排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,基准数据排在这两个子序列的中间;
然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
1 | // O(n*logn) |
注意:一个元素,可以看做有序的,是稳定的算法
对一个数组分成两路,mid中间
设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:R[low..m],R[m+1..high],先将它们合并到一个局部的暂存向量R1(相当于输出堆)中,待合并完成后将R1复制回R[low..high]中。
C++模板是容器的概念。
理论提高:所有容器提供的都是值(value)语意,而非引用(reference)语意。容器执行插入元素的操作时,内部实施拷贝动作。所以STL容器内存储的元素必须能够被拷贝(必须提供拷贝构造函数)。
加入到容器中的元素,应该可以被加入才行。
模板类设计与实现
链表类_链式存储设计与实现
栈类_链式存储设计与实现
队列类_链式存储设计与实现
]]>链表类_顺序存储设计与实现
栈类_顺序存储设计与实现
队列类_顺序存储设计与实现
栈区
堆区
1 |
|
全局/静态区
全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量、静态变量和常量。
注意:
1 | int v1 = 10;//全局/静态区 |
字符串常量是否可修改?字符串常量优化:
ANSI C中规定:修改字符串常量,结果是未定义的。 ANSI C并没有规定编译器的实现者对字符串的处理,例如: 1. 有些编译器可修改字符串常量,有些编译器则不可修改字符串常量。 2. 有些编译器把多个相同的字符串常量看成一个(这种优化可能出现在字符串常量中,节省空间),有些则不进行此优化。如果进行优化,则可能导致修改一个字符串常量导致另外的字符串常量也发生变化,结果不可知。 所以尽量不要去修改字符串常量! |
---|
C99标准: char p = “abc”; defines p with type ‘‘pointer to char’’ and initializes it to point to an object with type ‘‘array of char’’ with length 4 whose elements are initialized with a character string literal. *If an attempt is made to use p to modify the contents of the array, the behavior is undefined**. |
总结
在理解C/C++内存分区时,常会碰到如下术语:数据区,堆,栈,静态区,常量区,全局区,字符串常量区,文字常量区,代码区等等,初学者被搞得云里雾里。在这里,尝试捋清楚以上分区的关系。
数据区包括:堆,栈,全局/静态存储区。
全局/静态存储区包括:常量区,全局区、静态区。
常量区包括:字符串常量区、常变量区。
代码区:存放程序编译后的二进制代码,不可寻址区。
可以说,C/C++内存分区其实只有两个,即代码区和数据区。
函数调用模型:
栈在程序运行中具有极其重要的地位。最重要的,栈保存一个函数调用所需要维护的信息,这通常被称为堆栈帧(Stack Frame)或者活动记录(Activate Record).一个函数调用过程所需要的信息一般包括以下几个方面:
栈的生长方向和内存存放方向:
指针是一种数据类型,占用内存空间,用来保存内存地址。
标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可以给它赋值一个零值。为了测试一个指针百年来那个是否为NULL,你可以将它与零值进行比较。
对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未执行任何东西,因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NULL指针。
如果对一个NULL指针间接访问会发生什么呢?结果因编译器而异。
不允许向NULL和非法地址拷贝内存:
1 | void test(){ |
在使用指针时,要避免野指针的出现:
野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。
什么情况下会导致野指针?
指针变量未初始化
指针释放后未置空
指针操作超越变量作用域
操作野指针是非常危险的操作,应该规避野指针的出现:
初始化时置 NULL
释放时置 NULL
用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。请看下面的例子:
1 |
|
运行结果:
1 | value = 100 |
n 是 func() 内部的局部变量,func() 返回了指向 n 的指针,根据上面的观点,func() 运行结束后 n 将被销毁,使用 *p
应该获取不到 n 的值。但是从运行结果来看,我们的推理好像是错误的,func() 运行结束后 *p
依然可以获取局部变量 n 的值,这个上面的观点不是相悖吗?
为了进一步看清问题的本质,不妨将上面的代码稍作修改,在第9~10行之间增加一个函数调用,看看会有什么效果:
1 |
|
运行结果:
1 | c.biancheng.net |
可以看到,现在 p 指向的数据已经不是原来 n 的值了,它变成了一个毫无意义的甚至有些怪异的值。与前面的代码相比,该段代码仅仅是在 *p
之前增加了一个函数调用,这一细节的不同却导致运行结果有天壤之别,究竟是为什么呢?
前面我们说函数运行结束后会销毁所有的局部数据,这个观点并没错,大部分C语言教材也都强调了这一点。但是,这里所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。对于上面的两个例子,func() 运行结束后 n 的内存依然保持原样,值还是 100,如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据就失去了意义。
关于函数调用的原理以及函数如何占用内存的更多细节,我们将在《C语言和内存》专题中深入探讨,相信你必将有所顿悟,解开心中的谜团。
第一个例子在调用其他函数之前使用 *p
抢先获得了 n 的值并将它保存起来,第二个例子显然没有抓住机会,有其他函数被调用后才使用 *p
获取数据,这个时候已经晚了,内存已经被后来的函数覆盖了,而覆盖它的究竟是一份什么样的数据我们无从推断(一般是一个没有意义甚至有些怪异的值)。
总结:
常规程序中,函数返回的指针通常应该是:
除这5项以外,其它怪技巧不提倡。
函数内的变量,没有关键字static修饰的变量的生命周期只在本函数内,函数结束后变量自动销毁。当返回为指针的时候需要特别注意,因为函数结束后指针所指向的地址依然存在,但是该地址可以被其他程序修改,里面的内容就不确定了,有可能后面的操作会继续用到这块地址,有可能不会用到,所以会出现时对时错的情况,如果需要返回一个指针而又不出错的话只能调用内存申请函数
通过一个指针访问它所指向的地址的过程叫做间接访问,或者叫解引用指针,这个用于执行间接访问的操作符是 *
。
注意:对一个int*
类型指针解引用会产生一个整型值,类似地,对一个float*
指针解引用会产生了一个float类型的值。
在指针声明时,*
号表示所声明的变量为指针
在指针使用时,*
号表示操作指针所指向的内存空间
*
相当通过地址(指针变量的值)找到指针指向的内存,再操作内存*
放在等号的左边赋值(给内存赋值,写内存)*
放在等号的右边取值(从内存中取值,读内存)1 | //解引用 |
指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。
通过指针间接赋值成立的三大条件:
2个变量(一个普通变量一个指针变量、或者一个实参一个形参)
建立关系
通过 *
操作指针指向的内存
1 | void test(){ |
间接赋值:从1级指针到2级指针:
1 | void AllocateSpace(char** p){ |
间接赋值的推论:
用 1 级指针形参,去间接修改了 0 级指针(实参)的值。
用 2 级指针形参,去间接修改了 1 级指针(实参)的值。
用 3 级指针形参,去间接修改了 2 级指针(实参)的值。
用 n 级指针形参,去间接修改了 n-1 级指针(实参)的值。
指针做函数参数,具备输入和输出特性:
输入:主调函数分配内存
输出:被调用函数分配内存
输入特性:
1 | void fun(char *p /* in */) |
输出特性:
1 | void fun(char **p /* out */, int *len) |
字符串是以0或者’\0’结尾的字符数组,(数字0和字符’\0’等价)
如果以字符串初始化,那么编译器默认会在字符串尾部添加’\0’
1 | char str3[] = "hello"; |
sizeof 计算数组大小,数组包含’\0’字符
strlen 计算字符串的长度,到’\0’结束
字符串拷贝功能实现:
1 | //1)应该判断下传入的参数是否为NULL |
字符串的格式化:
1 |
|
1 | //1. 格式化字符串 |
1 |
|
格式 | 作用 |
---|---|
%*s或%*d | 跳过数据 |
%[width]s | 读指定宽度的数据 |
%[a-z] | 匹配a到z中任意字符(尽可能多的匹配) |
%[aBc] | 匹配a、B、c中一员,贪婪性 |
%[^a] | 匹配非a的任意字符,贪婪性 |
%[^a-z] | 表示读取除a-z以外的所有字符 |
1 | //1. 跳过数据 |
越界
指针叠加会不断改变指针指向 p++
返回局部变量地址
1 | char *get_str() |
1 | //const修饰变量 |
1 | int a = 12; |
它在内存中的大概模样大致如下:
二级指针做参数的输出特性是指由被调函数分配内存。
1 | //被调函数,由参数n确定分配多少个元素内存 |
二级指针做形参输入特性是指由主调函数分配内存。
1 | //打印数组 |
4个位运算符用于整型数据,包括char.将这些位运算符成为位运算的原因是它们对每位进行操作,而不影响左右两侧的位。请不要将这些运算符与常规的逻辑运算符(&& 、||和!)相混淆,常规的位的逻辑运算符对整个值进行操作。
1 | unsigned char a = 2; //00000010 |
位与(AND): &
位或(OR): |
位异或:
^
对两个操作数逐位进行比较。对于每个位,如果操作数中的对应位有一个是1(但不是都是1),那么结果是1.如果都是0或者都是1,则结果位0.1 | (10010011) |
用法:
已知:10011010:
将位2打开
flag | 10011010
1 | (10011010) |
将所有位打开。
flag | ~flag
1 | (10011010) |
关闭位
flag & ~flag
1 | (10011010) |
转置位
b^1
为0,如果b为0,则 1^b
为1。无论b的值是0还是1, 0^b
为b.flag ^ 0xff
1 | (10010011) |
1 | //a ^ b = temp; |
左移 <<
左移运算符 <<
将其左侧操作数的值的每位向左移动,移动的位数由其右侧操作数指定。空出来的位用0填充,并且丢弃移出左侧操作数末端的位。在下面例子中,每位向左移动两个位置。
左移一位相当于原值 *2
.
1 | (10001010) << 2 |
>>
将其左侧的操作数的值每位向右移动,移动的位数由其右侧的操作数指定。丢弃移出左侧操作数有段的位。对于unsigned类型,使用0填充左端空出的位。对于有符号类型,结果依赖于机器。空出的位可能用0填充,或者使用符号(最左端)位的副本填充。1 | //有符号值 |
用法:移位运算符:
number << n | number乘以2的n次幂 |
---|---|
number >> n | 如果number非负,则用number除以2的n次幂 |
请问:指针和数组是等价的吗?
答案是否定的。数组名在表达式中使用的时候,编译器才会产生一个指针常量。那么数组在什么情况下不能作为指针常量呢?在以下两种场景下:
1 | int arr[10]; |
下标引用:
1 | int arr[] = { 1, 2, 3, 4, 5, 6 }; |
1 | *(arr + 3) |
问题 1:数组下标可否为负值?
问题 2:请阅读如下代码,说出结果:
1 | int arr[] = { 5, 3, 6, 8, 2, 9 }; |
指针和数组并不是相等的。为了说明这个概念,请考虑下面两个声明
1 | int a[10]; |
声明一个数组时,编译器根据声明所指定的元素数量为数组分配内存空间,然后再创建数组名,指向这段空间的起始位置。声明一个指针变量的时候,编译器只为指针本身分配内存空间,并不为任何整型值分配内存空间,指针并未初始化指向任何现有的内存空间。
因此,表达式 *a
是完全合法的,但是表达式 *b
却是非法的。*b
将访问内存中一个不确定的位置,将会导致程序终止。另一方面 b++ 可以通过编译,a++ 却不行,因为 a 是一个常量值。
当一个数组名作为一个参数传递给一个函数的时候发生什么情况呢?我们现在知道数组名其实就是一个指向数组第1个元素的指针,所以很明白此时传递给函数的是一份指针的拷贝。所以函数的形参实际上是一个指针。但是为了使程序员新手容易上手一些,编译器也接受数组形式的函数形参。因此下面两种函数原型是相等的:
1 | int print_array(int *arr); |
我们可以使用任何一种声明,但哪一个更准确一些呢?答案是指针。因为实参实际上是个指针,而不是数组。同样sizeof arr值是指针的长度,而不是数组的长度。
现在我们清楚了,为什么一维数组中无须写明它的元素数目了,因为形参只是一个指针,并不需要为数组参数分配内存。另一方面,这种方式使得函数无法知道数组的长度。如果函数需要知道数组的长度,它必须显式传递一个长度参数给函数。
数组名:
1 | int arr[3][10] |
可以理解为这是一个一维数组,包含了3个元素,只是每个元素恰好是包含了10个元素的数组。arr就表示指向它的第1个元素的指针,所以arr是一个指向了包含了10个整型元素的数组的指针。
指向数组的指针(数组指针):
数组指针,它是指针,指向数组的指针。
数组的类型由元素类型和数组大小共同决定:int array[5] 的类型为 int[5];C语言可通过typedef定义一个数组类型:
定义数组指针有一下三种方式:
1 | //方式一 |
栈区指针数组:
1 | //数组做函数函数,退化为指针 |
堆区指针数组:
1 | //分配内存 |
二维数组的线性存储特性式:
1 | void PrintArray(int* arr, int len){ |
二维数组的3种形式参数:
1 | //二维数组的第一种形式 |
编程提示:
内容总结:
结构体类型的定义
1 | struct Person{ |
注意:定义结构体类型时不要直接给成员赋值,结构体只是一个类型,编译器还没有为其分配空间,只有根据其类型定义变量时,才分配空间,有空间后才能赋值。
结构体变量的定义
1 | struct Person{ |
结构体成员的使用
1 | struct Person{ |
深拷贝和浅拷贝
1 | //一个老师有N个学生 |
结构体数组
1 | struct Person{ |
结构体嵌套一级指针
1 | struct Person{ |
结构体嵌套二级指针
1 | //一个老师有N个学生 |
1 | //一旦结构体定义下来,则结构体中的成员内存布局就定下了 |
在用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题。
从理论上讲,对于任何变量的访问都可以从任何地址开始访问,但是事实上不是如此,实际上访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按一定的规则排列, 而不是简单地顺序排列,这就是内存对齐。
我们知道内存的最小单元是一个字节,当cpu从内存中读取数据的时候,是一个一个字节读取,但是实际上cpu将内存当成多个块,每次从内存中读取一个块,这个块的大小可能是2、4、8、16等
内存对齐是操作系统为了提高访问内存的策略。操作系统在访问内存的时候,每次读取一定长度(这个长度是操作系统默认的对齐数,或者默认对齐数的整数倍)。如果没有对齐,为了访问一个变量可能产生二次访问。
为什么要简单内存对齐?
手动设置对齐模数:
内存对齐案例
1 |
|
文件在今天的计算机系统中作用是很重要的。文件用来存放程序、文档、数据、表格、图片和其他很多种类的信息。作为一名程序员,您必须编程来创建、写入和读取文件。编写程序从文件读取信息或者将结果写入文件是一种经常性的需求。C提供了强大的和文件进行通信的方法。使用这种方法我们可以在程序中打开文件,然后使用专门的I/O函数读取文件或者写入文件。
文件的概念
流的概念
流是一个动态的概念,可以将一个字节形象地比喻成一滴水,字节在设备、文件和程序之间的传输就是流,类似于水在管道中的传输,可以看出,流是对输入输出源的一种抽象,也是对传输信息的一种抽象。
C语言中,I/O操作可以简单地看作是从程序移进或移出字节,这种搬运的过程便称为流(stream)。程序只需要关心是否正确地输出了字节数据,以及是否正确地输入了要读取字节数据,特定I/O设备的细节对程序员是隐藏的。
文本流
二进制流
c语言在处理这两种文件的时候并不区分,都看成是字符流,按字节进行处理。
我们程序中,经常看到的文本方式打开文件和二进制方式打开文件仅仅体现在换行符的处理上。
比如说,在widows下,文件的换行符是 \r\n
,而在Linux下换行符则是 \n
.
当对文件使用文本方式打开的时候,读写的windows文件中的换行符\r\n会被替换成\n读到内存中,当在windows下写入文件的时候,\n被替换成\r\n再写入文件。如果使用二进制方式打开文件,则不进行\r\n和\n之间的转换。 那么由于Linux下的换行符就是\n, 所以文本文件方式和二进制方式无区别。
文件流总览
标准库函数是的我们在C程序中执行与文件相关的I/O任务非常方便。下面是关于文件I/O的一般概况。
FILE*
。这个指针指向这个FILE结构,当它处于活动状态时由流使用。标准I/O更为简单,因为它们并不需要打开或者关闭。
I/O函数以三种基本的形式处理数据:单个字符、文本行和二进制数据。对于每种形式都有一组特定的函数对它们进行处理。
输入/输出函数家族
家族名 | 目的 | 可用于所有流 | 只用于stdin和stdout |
---|---|---|---|
getchar | 字符输入 | fgetc、getc | getchar |
putchar | 字符输出 | fputc、putc | putchar |
gets | 文本行输入 | fgets | gets |
puts | 文本行输出 | fputs | puts |
scanf | 格式化输入 | fscanf | scanf |
printf | 格式化输出 | fprintf | printf |
文件的打开操作表示将给用户指定的文件在内存分配一个FILE结构区,并将该结构的指针返回给用户程序,以后用户程序就可用此FILE指针来实现对指定文件的存取操作了。当使用打开函数时,必须给出文件名、文件操作方式(读、写或读写)。
1 | FILE * fopen(const char * filename, const char * mode); |
方式 | 含义 |
---|---|
“r” | 打开,只读,文件必须已经存在。 |
“w” | 只写,如果文件不存在则创建,如果文件已存在则把文件长度截断(Truncate)为0字节。再重新写,也就是替换掉原来的文件内容文件指针指到头。 |
“a” | 只能在文件末尾追加数据,如果文件不存在则创建 |
“rb” | 打开一个二进制文件,只读 |
“wb” | 打开一个二进制文件,只写 |
“ab” | 打开一个二进制文件,追加 |
“r+” | 允许读和写,文件必须已存在 |
“w+” | 允许读和写,如果文件不存在则创建,如果文件已存在则把文件长度截断为0字节再重新写 。 |
“a+” | 允许读和追加数据,如果文件不存在则创建 |
“rb+” | 以读/写方式打开一个二进制文件 |
“wb+” | 以读/写方式建立一个新的二进制文件 |
“ab+” | 以读/写方式打开一个二进制文件进行追加 |
1 | void test(){ |
注意:应该检查fopen的返回值!如何函数失败,它会返回一个NULL值。如果程序不检查错误,这个NULL指针就会传给后续的I/O函数。它们将对这个指针执行间接访问,并将失败.
1 | int fclose(FILE * stream); |
它表示该函数将关闭FILE指针对应的文件,并返回一个整数值。若成功地关闭了文件,则返回一个0值,否则返回一个非0值.
文件读写函数回顾
块读写函数回顾
1 | size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); |
格式化读写函数回顾
1 | int fprintf(FILE * stream, const char * format, ...); |
注意:fscanf遇到空格和换行时结束。
1 | struct info{ |
数组和链表的区别:
数组:一次性分配一块连续的存储区域。
优点:随机访问元素效率高
缺点:
链表:无需一次性分配一块连续的存储区域,只需分配n块节点存储区域,通过指针建立关系。
优点:
缺点:随机访问元素效率低
问题1:请问结构体可以嵌套本类型的结构体变量吗?
问题2:请问结构体可以嵌套本类型的结构体指针变量吗?
1 | typedef struct _STUDENT{ |
大家思考一下,我们说链表是由一系列的节点组成,那么如何表示一个包含了数据域和指针域的节点呢?
链表的节点类型实际上是结构体变量,此结构体包含数据域和指针域:
数据域用来存储数据;
指针域用于建立与下一个结点的联系,当此节点为尾节点时,指针域的值为NULL;
1 | typedef struct Node |
链表分为:
静态链表和动态链表是线性表链式存储结构的两种不同的表示方式:
所有结点都是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表称为“静态链表”。
所谓动态链表,是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各结点数据,并建立起前后相链的关系。
静态链表
1 | typedef struct Stu { |
动态链表
1 | typedef struct Stu { |
带头和不带头链表
单向链表、双向链表、循环链表
使用结构体定义节点类型:
1 | typedef struct _LINKNODE |
编写函数:link_node* init_linklist()
建立带有头结点的单向链表,循环创建结点,结点数据域中的数值从键盘输入,以 -1 作为输入结束标志,链表的头结点地址由函数值返回.
1 | typedef struct _LINKNODE{ |
编写函数:void foreach_linklist(link_node* head)
顺序输出单向链表各项结点数据域中的内容:
1 | //遍历链表 |
编写函数: void insert_linklist(link_node* head,int val,int data).
在指定值后面插入数据data,如果值val不存在,则在尾部插入。
1 | //在值val前插入节点 |
编写函数: void remove_linklist(link_node* head,int val)
删除第一个值为val的结点.
1 | //删除值为val的节点 |
编写函数: void destroy_linklist(link_node* head)
销毁链表,释放所有节点的空间.
1 | //销毁链表 |
通过什么来区分两个不同的函数?
一个函数在编译时被分配一个入口地址,这个地址就称为函数的指针,函数名代表函数的入口地址。
函数三要素: 名称、参数、返回值。C语言中的函数有自己特定的类型。
c 语言中通过 typedef 为函数类型重命名:
1 | typedef int f(int, int);// f 为函数类型 |
这一点和数组一样,因此我们可以用一个指针变量来存放这个入口地址,然后通过该指针变量调用函数。
注意:通过函数类型定义的变量是不能够直接执行,因为没有函数体。只能通过类型定义一个函数指针指向某一个具体函数,才能调用。
1 | typedef int(p)(int, int); |
1 | int my_func(int a,int b){ |
函数指针数组,每个元素都是函数指针。
1 | void func01(int a){ |
函数参数除了是普通变量,还可以是函数指针变量。
1 | //形参为普通变量 |
函数指针变量常见的用途之一是把指针作为参数传递到其他函数,指向函数的指针也可以作为参数,以实现函数地址的传递。
1 | //加法计算器 |
注意:函数指针和指针函数的区别:
函数指针是指向函数的指针;
指针函数是返回类型为指针的函数;
C 语言对源程序处理的四个步骤:预处理、编译、汇编、链接。
预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理。这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成为特定的符号为下一步的编译做准备工作。
“文件包含处理”是指一个源文件可以将另外一个文件的全部内容包含进来。C语言提供了 #include 命令用来实现“文件包含”的操作。
#incude<> 和 #include”” 区别
“” 表示系统先在 file1.c 所在的当前目录找 file1.h,如果找不到,再按系统指定的目录检索。
< > 表示系统直接按系统指定的目录检索。
注意:
1. #include <> 常用于包含库函数的头文件;
2. #include “” 常用于包含自定义的头文件;
3. 理论上 #include 可以包含任意格式的文件(.c .h等) ,但一般用于头文件的包含;
如果在程序中大量使用到了100这个值,那么为了方便管理,我们可以将其定义为:
const int num = 100; 但是如果我们使用num定义一个数组,在不支持c99标准的编译器上是不支持的,因为num不是一个编译器常量,如果想得到了一个编译器常量,那么可以使用:
#define num 100
在编译预处理时,将程序中在该语句以后出现的所有的num都用100代替。这种方法使用户能以一个简单的名字代替一个长的字符串,在预编译时将宏名替换成字符串的过程称为“宏展开”。宏定义,只在宏定义的文件中起作用。
1 |
|
说明:
1)宏名一般用大写,以便于与变量区别;
2) 宏定义可以是常数、表达式等;
3) 宏定义不作语法检查,只有在编译被宏展开后的源程序才会报错;
4) 宏定义不是C语言,不在行末加分号;
5) 宏名有效范围为从定义到本源文件结束;
6) 可以用#undef命令终止宏定义的作用域;
7) 在宏定义中,可以引用已定义的宏名;
在项目中,经常把一些短小而又频繁使用的函数写成宏函数,这是由于宏函数没有普通函数参数压栈、跳转、返回等的开销,可以调高程序的效率。
宏通过使用参数,可以创建外形和作用都与函数类似地类函数宏(function-like macro). 宏的参数也用圆括号括起来。
1 |
|
注意:
1) 宏的名字中不能有空格,但是在替换的字符串中可以有空格。ANSI C允许在参数列表中使用空格;
2) 用括号括住每一个参数,并括住宏的整体定义。
3) 用大写字母表示宏的函数名。
4) 如果打算宏代替函数来加快程序运行速度。假如在程序中只使用一次宏对程序的运行时间没有太大提高。
一般情况下,源程序中所有的行都参加编译。但有时希望对部分源程序行只在满足一定条件时才编译,即对这部分源程序行指定编译条件。
条件编译
1 |
|
C 编译器,提供了几个特殊形式的预定义宏,在实际编程中可以直接使用,很方便。
1 | //__FILE__宏所在文件的源文件名 |
库是已经写好的、成熟的、可复用的代码。每个程序都需要依赖很多底层库,不可能每个人的代码从零开始编写代码,因此库的存在具有非常重要的意义。
在我们的开发的应用中经常有一些公共代码是需要反复使用的,就把这些代码编译为库文件。
库可以简单看成一组目标文件的集合,将这些目标文件经过压缩打包之后形成的一个文件。像在Windows这样的平台上,最常用的 c 语言库是由集成按开发环境所附带的运行库,这些库一般由编译厂商提供。
库:就是已经编写好的,后续可以直接使用的代码。
c++静态库:会合入到最终生成的程序,使得结果文件比较大。优点是不再有任何依赖。
c++动态库:动态库,一个文件可以多个代码同时使用内存中只有一份,节省内存,可以随主代码一起编译。缺点是需要头文件。
网友说:库就是除了main函数之外的其他代码,都可以组成库。
静态库对函数库的链接是放在编译时期完成的,静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系;
程序在运行时与函数库再无瓜葛,移植方便。
浪费空间和资源,所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
内存和磁盘空间
程序开发和发布
要解决空间浪费和更新困难这两个问题,最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不是将他们静态的链接在一起。简单地讲,就是不对哪些组成程序的目标程序进行链接,等程序运行的时候才进行链接。也就是说,把整个链接过程推迟到了运行时再进行,这就是动态链接的基本思想。
我们通常把一些公用函数制作成函数库,供其它程序使用。函数库分为静态库和动态库两种。
静态库在程序编译时会被链接并拷贝到目标代码中,程序运行时将不再需要该静态库。
动态库在程序编译时并不会被拷贝到目标代码中,而是在程序运行时才被载入,因此在程序运行时还需要动态库存在。本质上说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。
windows 和 linux 库的二进制是不兼容的(主要是编译器、汇编器和连接器的不同)。
库的种类:
linux下的库有两种:
二者区别在于代码被载入的时刻不同。静态库的代码在编译过程中已经被载入可执行程序,因此体积较大。共享库的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小。
库文件是如何产生的:
静态库的后缀是 .a
,它的产生分两步:
Step 1. 由源文件编译生成一堆 .o
,每个 .o
里都包含这个编译单元的符号表
Step 2. ar 命令将很多 .o
转换成 .a
,成为静态库
动态库的后缀是 .so
,它由 gcc 加特定参数编译产生。
库文件命名规范:
库文件一般放在 /usr/local/lib
,/usr/lib
,/lib
,或者其他自定义的 lib
下。
静态库的名字一般为 libxxxx.a
,其中 xxxx
是该 lib
的名称
动态库的名字一般为 libxxxx.so.major.minor
, xxxx
是该 lib
的名称,major
是主版本号, minor
是副版本号
如何知道一个可执行程序依赖哪些库:
ldd
命令可以查看一个可执行程序依赖的共享库,例如:
1 | ldd /lib/i386-linux-gnu/libc.so.6 |
可以看到 libc
命令依赖于 linux-gate
库和 ld-linux
库
可执行程序在执行的时候如何定位共享库文件:
当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统动态载入器(dynamic linker/loader
)
对于 elf
格式的可执行程序,是由 ld-linux.so*
来完成的,它先后搜索 elf
文件的 DT_RPATH
段—环境变量 LD_LIBRARY_PATH—/etc/ld.so.cache
文件列表— /lib/,/usr/lib
目录找到库文件后将其载入内存
如:export LD_LIBRARY_PATH=’pwd’
将当前文件目录添加为共享目录
在新安装一个库之后如何让系统能够找到他:
如果安装在 /lib
或者 /usr/lib
下,那么 ld
默认能够找到,无需其他操作。如果安装在其他目录,需要将其添加到 /etc/ld.so.cache
文件中,步骤如下:
编辑 /etc/ld.so.conf
文件,加入库文件所在目录的路径
运行 ldconfig
,该命令会重建 /etc/ld.so.cache
文件
假设有1个类 hello,和一个 main 函数。如下:
hello.h
1 |
|
hello.c
1 |
|
main.c
1 |
|
hello.c 是一个没有 main 函数的 .c
程序,因此不够成一个完整的程序,如果使用 gcc –o
编译并连接它,gcc
将报错,无法通过编译。
前面提过,无论静态库,还是动态库,都是由 .o
文件创建的。那么我们如何才能让 main.c 调用 hello 类呢?也就是说该如何才能将 hello.c 通过 gcc 先编译成 .o
文件,并且让 main.c 在编译时能找到它?有三种途径可以实现:
1)通过编译多个源文件,直接将目标代码合成一个 .o
文件。
2)通过创建静态链接库 libmyhello.a
,使得 main 函数调用 hello 函数时可调用静态链接库。
3)通过创建动态链接库 libmyhello.so
,使得 main 函数调用 hello 函数时可调用动态链接库。
执行命令:
1 | gcc -c hello.c |
这里提醒一下:gcc –o
是将 .c
源文件编译成为一个可执行的二进制代码。而 gcc –c
是使用GNU汇编器将源文件转化为目标代码。更多 gcc 编译选项的常识点这里。
这时可以看到生成了 hello.o 和 main.o 文件。
1 | ls |
静态库文件名是以 lib 为前缀,紧接着是静态库名,扩展名为 .a
。例如:我们将创建的静态库名为myhello,则静态库文件名就是 libmyhello.a
。创建静态库用 ar
命令。
删除途径一中生成的3个文件,回到原始的三个文件:
1 | rm hello.o main.o sayhello |
静态库制作完了,如何使用它内部的函数呢?
只需要在使用到这些公用函数的源程序中包含这些公用函数的原型声明,然后在用 gcc 命令生成目标文件时指明静态库名,gcc 将会从静态库中将公用函数连接到目标文件中。
注意,gcc 会在静态库名前加上前缀 lib,然后追加扩展名 .a
得到的静态库文件名来查找静态库文件。
因此,我们在写需要连接的库时,只写静态库名就可以,如 libmyhello.a
的库,只写: -lmyhello
在 main.c 中,我们已包含了该静态库的头文件 hello.h。现在在主程序 main.c 中直接调用它内部的函数:
1 | 这里-L.告诉 gcc 先在当前目录下查找库文件。 |
前面提过静态库在编译过程中会被拷贝到目标程序中,运行时不再需要静态库的存在。这里可以简单验证一下:我们删除静态库文件,然后再试着调用函数 hello 看是否还能调用成功。
1 | rm libmyhello.a |
程序照常运行,静态库中的函数已经被复制到目标程序中了,编译完成后,静态库就没用了,执行时不再需要静态库的存在。
静态链接库的一个缺点是:
使用了共享链接库的Linux就可以避免这个问题。共享函数库和静态函数在同一个地方,只是后缀不同。比如,在Linux系统,标准的共享数序函数库是 /usr/lib/libm.so
。当一个程序使用共享函数库时,在连接阶段并不把函数代码连接进来,而只是链接函数的一个引用。当最终的函数导入内存开始真正执行时,函数引用被解析,共享函数库的代码才真正导入到内存中。这样,共享链接库的函数就可以被许多程序同时共享,并且只需存储一次就可以了。共享函数库的另一个优点是,它可以独立更新,与调用它的函数毫不影响。
动态库文件名和静态库类似,也是在动态库名增加前缀 lib,但其文件扩展名为 .so
。例如:我们将创建的动态库名为 myhello,则动态库文件名就是 libmyhello.so
。用 gcc 来创建动态库。
删除途径二中生成的2个文件,回到原始的三个文件:
1 | rm hello.o sayhello |
正确方法是,这样就可以了:
1 | gcc -fPIC -shared -o libmyhello.so hello.c |
最主要的是 GCC 命令行的选项:
-shared
:指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件
-fPIC
:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
下面调用该动态链接库:
1 | gcc -o sayhello main.c -L. -lmyhello |
按教程里说的:他以这种方式调用动态链接库出错,找不到动态库文件 libmyhello.so
:
1 | ./sayhello: error while loading shared libraries: libmyhello.so: cannot open shared object file: No such file or directory |
程序在运行时,会在 /usr/lib 和 /lib 等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提示类似上述错误而终止程序运行。解决此类问题有如下三种方法:
(1)我们将文件 libmyhello.so复制到目录/usr/lib中。
(2)既然连接器会搜寻LD_LIBRARY_PATH所指定的目录,那么我们只要将当前目录添加到环境变量:
export LD_LIBRARY_PATH=$(pwd)
(3)执行: ldconfig /usr/zhsoft/lib
说明:当用户在某个目录下面创建或拷贝了一个动态链接库,若想使其被系统共享,可以执行一下 “ldconfig 目录名” 这个命令。此命令的功能在于让 ldconfig 将指定目录下的动态链接库被系统共享起来,意即:在缓存文件 /etc/ld.so.cache
中追加进指定目录下的共享库。该命令会重建 /etc/ld.so.cache
文件。
参考教程:
http://blog.csdn.net/jiayouxjh/article/details/7602729
http://blog.sina.com.cn/s/blog_54f82cc20101153x.html
http://navyaijm.blog.51cto.com/4647068/809424
C通过运行时堆栈来支持递归函数的实现。递归函数就是直接或间接调用自身的函数。
1 | void funB(int b){ |
函数的调用流程如下:
1 | void fun(int a){ |
函数的调用流程如下:
递归实现给出一个数8793,依次打印千位数字8、百位数字7、十位数字9、个位数字3。
1 | void recursion(int val){ |
1 | int reverse1(char *str){ |
TODO
一般的企业信息系统都有成熟的框架。软件框架一般不发生变化,能自由的集成第三方厂商的产品。
要求在企业信息系统框架中集成第三方厂商的socket通信产品和第三方厂商加密产品。软件设计要求:模块要求松、接口要求紧。
1)能支持多个厂商的 socket 通信产品入围
2)能支持多个第三方厂商加密产品的入围
3)企业信息系统框架不轻易发生框架
1)抽象通信接口结构体设计(CSocketProtocol)
2)框架接口设计(framework)
3) a) 通信厂商1入围(CSckImp1) b) 通信厂商2入围(CSckImp2)
4) a) 抽象加密接口结构体设计(CEncDesProtocol) b) 升级框架函数(增加加解密功能) c) 加密厂商1入围(CHwImp)、加密厂商2入围(CCiscoImp)
5)框架接口分文件
声明变量不需要建立存储空间,如:extern int a;
定义变量需要建立存储空间,如:int b;
全局数组若不初始化,编译器将其初始化为零。局部数组若不初始化,内容为随机值。
数字 0 (和字符 ‘\0’ 等价)结尾的char数组就是一个字符串,但如果char数组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的char的数组。
gets(str)与scanf(“%s”,str)的区别:
gets(str)允许输入的字符串含有空格
scanf(“%s”,str)不允许含有空格
注意:由于scanf()和gets()无法知道字符串s大小,必须遇到换行符或读到文件结尾为止才接收输入,因此容易导致字符数组越界(缓冲区溢出)的情况。
gets() 、puts()
1 |
|
fgets() 、fputs()
1 |
|
strlen() 、strcpy() 、strncpy() 、strcat() 、strncat() 、strcmp() 、strncmp() 、sprintf() 、sscanf() 、strchr() 、strstr() 、strtok() 、atoi()
1 |
|
形参列表
如果函数返回的类型和return语句中表达式的值不一致,则以函数返回类型为准,即函数返回类型决定返回值的类型。对数值型数据,可以自动进行类型转换。
注意:如果函数返回的类型和return语句中表达式的值不一致,而它又无法自动进行类型转换,程序则会报错。
当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
extern告诉编译器这个变量或函数在其他文档里已被定义了。
static法则:
1 | // 多文件编译 |
指针大小
野指针和空指针
#define NULL ((void *)0)
万能指针 void *
void *
指针可以指向任意变量的内存空间:
1 | void *p = NULL; |
const修饰的指针变量
1 | int a = 100; |
指针操作数组元素
1 |
|
指针加减运算
int *
,+1的结果是增加一个int的大小char *
,+1的结果是增加一个char大小通过改变指针指向操作数组元素:
1 |
|
指针数组
1 |
|
多级指针
1 | int a = 10; |
数组名做函数参数
1 |
|
指针做为函数的返回值
1 |
|
指针和字符串
1 |
|
const修饰的指针变量
1 |
|
C语言变量的作用域分为:
局部变量也叫auto自动变量(auto可写可不写),一般情况下代码块{}内部定义的变量都是自动变量,它有如下特点:
1 |
|
静态(static)局部变量
全局变量
静态(static)全局变量
extern全局变量声明
全局函数和静态函数
注意:
总结:
类型 | 作用域 | 生命周期 |
---|---|---|
auto变量 | 一对{}内 | 当前函数 |
static局部变量 | 一对{}内 | 整个程序运行期 |
extern变量 | 整个程序 | 整个程序运行期 |
static全局变量 | 当前文件 | 整个程序运行期 |
extern函数 | 整个程序 | 整个程序运行期 |
static函数 | 当前文件 | 整个程序运行期 |
register变量 | 一对{}内 | 当前函数 |
内存分区
在 Linux 下,程序是一个普通的可执行文件,以下列出一个二进制可执行文件的基本情况:
通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。
代码区
全局初始化数据区/静态数据区(data段)
未初始化数据区(又叫 bss 区)
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
代码区(text segment)
未初始化数据区(BSS)
全局初始化数据区/静态数据区(data segment)
栈区(stack)
堆区(heap)
存储类型总结:
类型 | 作用域 | 生命周期 | 存储位置 |
---|---|---|---|
auto变量 | 一对{}内 | 当前函数 | 栈区 |
static局部变量 | 一对{}内 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern变量 | 整个程序 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
static全局变量 | 当前文件 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern函数 | 整个程序 | 整个程序运行期 | 代码区 |
static函数 | 当前文件 | 整个程序运行期 | 代码区 |
register变量 | 一对{}内 | 当前函数 | 运行时存储在CPU寄存器 |
字符串常量 | 当前文件 | 整个程序运行期 | data段 |
存储类型总结内存操作函数:
1 |
|
堆区内存分配和释放:
1 |
|
1 |
|
返回堆区地址:
1 |
|
定义结构体变量的方式:
结构体类型和结构体变量关系:
1 | //结构体类型的定义 |
结构体成员的使用:
1 |
|
结构体套结构体:
1 |
|
结构体套一级指针:
1 |
|
结构体普通变量做函数参数:
1 |
|
结构体指针变量做函数参数:
1 |
|
结构体数组名做函数参数:
1 |
|
枚举:将变量的值一一列举出来,变量的值只限于列举出来的值的范围内。
枚举类型定义:
1 | enum 枚举名 |
typedef为C语言的关键字,作用是为一种数据类型(基本类型或自定义数据类型)定义一个新名字,不能创建新类型。
与#define不同,typedef仅限于数据类型,而不是能是表达式或具体的值
#define发生在预处理,typedef发生在编译阶段
磁盘文件和设备文件
文件指针
1 | typedef struct |
FILE是系统使用typedef定义出来的有关文件信息的一种结构体类型,结构中含有文件名、文件状态和文件当前位置等信息。
声明FILE结构体类型的信息包含在头文件“stdio.h”中,一般设置一个指向FILE类型变量的指针变量,然后通过它来引用这些FILE类型变量。通过文件指针就可对它所指的文件进行各种操作。
文件的打开:
1 |
|
第二个参数的几种形式(打开文件的方式):
打开模式 | 含义 |
---|---|
r或rb | 以只读方式打开一个文本文件(不创建文件,若文件不存在则报错) |
w或wb | 以写方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件) |
a或ab | 以追加方式打开文件,在末尾添加内容,若文件不存在则创建文件 |
r+或rb+ | 以可读、可写的方式打开文件(不创建新文件) |
w+或wb+ | 以可读、可写的方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件) |
a+或ab+ | 以添加方式打开文件,打开文件并在末尾更改文件,若文件不存在则创建文件 |
注意:
b是二进制模式的意思,b只是在Windows有效,在Linux用r和rb的结果是一样的
Unix和Linux下所有的文本文件行都是\n结尾,而Windows所有的文本文件行都是\r\n结尾
在Windows平台下,以“文本”方式打开文件,不加b:
在Unix/Linux平台下,“文本”与“二进制”模式没有区别,”\r\n” 作为两个字符原样输入输出
1 | int main(void) |
1 |
|
在C语言中,EOF表示文件结束符(end of file)。在while循环中以EOF作为文件结束标志,这种以EOF作为文件结束标志的文件,必须是文本文件。在文本文件中,数据都是以字符的ASCII代码值的形式存放。我们知道,ASCII代码值的范围是0~127,不可能出现-1,因此可以用EOF作为文件结束标志。#define EOF (-1)
当把数据以二进制形式存放到文件中时,就会有-1值的出现,因此不能采用EOF作为二进制文件的结束标志。为解决这一个问题,ANSI C提供一个feof函数,用来判断文件是否结束。feof函数既可用以判断二进制文件又可用以判断文本文件。
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 | struct stat { |
1 |
|
1 |
|
ANSI C标准采用“缓冲文件系统”处理数据文件。
所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。
如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量) 。
磁盘文件的存取:
磁盘文件,一般保存在硬盘、U盘等掉电不丢失的磁盘设备中,在需要时调入内存
在内存中对文件进行编辑处理后,保存到磁盘中
程序与磁盘之间交互,不是立即完成,系统或程序可根据需要设置缓冲区,以提高存取效率
更新缓冲区:
1 |
|
1 | add2line:将你要找的地址转成文件和行号,它要使用 debug 信息 |
C调用C++库和C++调用C库的方法
C++ 调用 C 的函数比较简单,直接使用 extern "C" {}
告诉编译器用 C 的规则去调用 C 函数就可以了。
CAdd.h
1 | int cadd(int x, int y); |
CAdd.c
1 |
|
编译libCAdd.a
1 | gcc -c CAdd.c # 生成CAdd.o |
编译动态库 libCAdd.so
1 | gcc -shared -o libCAdd.so CAdd.c |
cppmain.cpp
1 |
|
编译main
-l
指定库名称,优先链接so动态库,没有动态库再链接 .a
静态库。
1 | g++ -o cppmain cppmain.cpp -L. -lCAdd |
运行
如果链接的是静态库就可以直接运行了,如果链接的是动态库可能会提示
1 | ./cppmain: error while loading shared libraries: libCAdd.so: cannot open shared object file: No such file or directory |
是因为Linux系统程序和Windows不一样,Linux系统只会从系统环境变量指定的路径加载动态库,可以把生成的动态库放到系统目录,或者执行 export LD_LIBRARY_PATH=./
设置当前路径为系统链接库目录就可以了。
*注释 *
这里是在 include 头文件的外面包裹了 extern "C" { }
,是告诉编译器以 C 语言的命名方式去加载这个符号。还有一种比较常见的方式是在头文件中进行编译声明,如下所示,这样的话,无论 C 还是 C++ 直接正常include就可以使用了。
CAdd.h
1 |
C 语言没法直接调用 C++ 的函数,但可以使用包裹函数来实现。C++ 文件 .cpp
中可以调用 C 和 C++ 的函数,但是 C 代码 .c
只能调用 C 的函数,所以可以用包裹函数去包裹C ++ 函数,然后把这个包裹函数以 C 的规则进行编译,这样 C 就可以调用这个包裹函数了。
CppAdd.h
1 | int cppadd(int x, int y); |
CppAdd.cpp
1 |
|
编译静态库 libCppAdd.a
1 | g++ -c CppAdd.cpp |
CppAddWrapper.h
1 |
|
CppAddWrapper.cpp
1 |
|
编译 wrapper 静态库 libCppAddWrapper.a
1 | g++ -c CppAddWrapper.cpp |
main.c
1 |
|
编译 main,同时指定 libCppAdd.a 和 libCppAddWrapper.a。
1 | gcc -o main main.c -L. -lCppAddWrapper -lCppAdd |
或者把 libCppAdd.a 合并到 libCppAddWrapper.a 中
1 | ar -x libCppAdd.a # 提取CppAdd.o |
如果是 C 调用 C++ 的 so 动态库的话,类似于调用静态库的方法应该也是有效的,太麻烦我没试过。
C/C++ 函数符号的区别
C++ 可以兼容 C 的语法,C/C++ 主要的区别是编译函数符号规则不一样,C 语言代码编译后的函数名还是原来函数名,C++ 代码编译后的函数名带有参数信息。
做个测试来检验一下。一个简单的函数,分别用 C 和 C++ 进行编译。
hello1.c
1 | int test(int a, char* b){ |
hello2.cpp
1 | int test(int a, char* b){ |
编译
1 | gcc -c hello1.c # 生成hello1.o |
查看符号表
1 | nm hello1.o |
从上面信息可以看出,C 语言编译后的函数符号还是原函数名,而 C++ 编译后的函数符号由test变成了 _Z4testiPc
,从这个符号名字可以看出 test 前面有个数字 4 应该是函数名长度,test 后面 iPc
应该就是函数的参数签名。C++ 之所以这样规定编译后的函数符号是因为对面对象的 C++ 具有函数重载功能,以此来区分不同的函数。
.so 动态库、.a 静态库和 .o 中间文件的关系
程序的运行都要经过编译和链接两个步骤。假如有文件 add.c
,可以使用命令 gcc -c add.c
进行编译,生成 add.o 中间文件,使用命令 ar -r libadd.a add.o
可以生成 libadd.a
静态库文件。静态库文件其实就是对 .o
中间文件进行的封装,使用 nm libadd.a
命令可以查看其中封装的中间文件以及函数符号。
链接静态库就是链接静态库中的 .o
文件,这和直接编译多个文件再链接成可执行文件一样。
动态链接库是程序执行的时候直接调用的“插件”,使用命令 gcc -shared -o libadd.so add.c
生成 so 动态库。动态库链接的时候可以像静态库一样链接,告诉编译器函数的定义在这个静态库中(避免找不到函数定义的错误),只是不把这个 so 打包到可执行文件中。如果没有头文件的话,可以使用 dlopen/dlsum
函数手动去加载相应的动态库。详细做法参考上一篇文章《C语言调用so动态库的两种方式》。
功能说明:建立或修改备存文件,或是从备存文件中抽取文件。
语 法:ar[-dmpqrtx][cfosSuvV][a<成员文件>][b<成员文件>][i<成员文件>][备存文件][成员文件]
补充说明:ar 可让您集合许多文件,成为单一的备存文件。在备存文件中,所有成员文件皆保有原来的属性与权限。
参 数:
1 | 指令参数 |
ar命令可以用来创建、修改库,也可以从库中提出单个模块。库是一单独的文件,里面包含了按照特定的结构组织起来的其它的一些文件(称做此库文件的member)。原始文件的内容、模式、时间戳、属主、组等属性都保留在库文件中。
下面是ar命令的格式:
1 | ar [-]{dmpqrtx}[abcfilNoPsSuvV][membername] [count] archive files... |
例如我们可以用ar rv libtest.a hello.o hello1.o来生成一个库,库名字是test,链接时可以用-ltest链接。该库中存放了两个模块hello.o和hello1.o。选项前可以有‘-‘字符,也可以没有。下面我们来看看命令的操作选项和任选项。现在我们把{dmpqrtx}部分称为操作选项,而[abcfilNoPsSuvV]部分称为任选项。
{dmpqrtx} 中的操作选项在命令中只能并且必须使用其中一个,它们的含义如下:
下面在看看可与操作选项结合使用的任选项:
nm用来列出目标文件的符号清单。下面是nm命令的格式:
1 | nm [-a|--debug-syms][-g|--extern-only] [-B][-C|--demangle] [-D|--dynamic][-s|--print-armap][-o|--print-file-name][-n|--numeric-sort][-p|--no-sort][-r|--reverse-sort] [--size-sort][-u|--undefined-only] [-l|--line-numbers][--help][--version][-t radix|--radix=radix][-P|--portability][-f format|--format=format][--target=bfdname][objfile...] |
如果没有为 nm 命令指出目标文件,则 nm 假定目标文件是a.out。下面列出该命令的任选项,大部分支持”-“开头的短格式和”—“开头的长格式。
-A、-o或–print-file-name:在找到的各个符号的名字前加上文件名,而不是在此文件的所有符号前只出现文件名一次。
例如nm libtest.a的输出如下:
1 | CPThread.o: |
-a或–debug-syms:显示调试符号。
-B:等同于–format=bsd,用来兼容MIPS的nm。
-C或–demangle:将低级符号名解码(demangle)成用户级名字。这样可以使得C++函数名具有可读性。
-D或–dynamic:显示动态符号。该任选项仅对于动态目标(例如特定类型的共享库)有意义。
-f format:使用format格式输出。format可以选取bsd、sysv或posix,该选项在GNU的nm中有用。默认为bsd。
-g或–extern-only:仅显示外部符号。
-n、-v或–numeric-sort:按符号对应地址的顺序排序,而非按符号名的字符顺序。
-p或–no-sort:按目标文件中遇到的符号顺序显示,不排序。
-P或–portability:使用POSIX.2标准输出格式代替默认的输出格式。等同于使用任选项-f posix。
-s或–print-armap:当列出库中成员的符号时,包含索引。索引的内容包含:哪些模块包含哪些名字的映射。
-r或–reverse-sort:反转排序的顺序(例如,升序变为降序)。
–size-sort:按大小排列符号顺序。该大小是按照一个符号的值与它下一个符号的值进行计算的。
-t radix或–radix=radix:使用radix进制显示符号值。radix只能为”d”表示十进制、”o”表示八进制或”x”表示十六进制。
–target=bfdname:指定一个目标代码的格式,而非使用系统的默认格式。
-u或–undefined-only:仅显示没有定义的符号(那些外部符号)。
-l或–line-numbers:对每个符号,使用调试信息来试图找到文件名和行号。对于已定义的符号,查找符号地址的行号。对于未定义符号,查找指向符号重定位入口的行号。如果可以找到行号信息,显示在符号信息之后。
-V或–version:显示nm的版本号。
–help:显示nm的任选项。
C 语言之解析局部变量返回
一般的来说,函数是可以返回局部变量的。 局部变量的作用域只在函数内部,在函数返回后,局部变量的内存已经释放了。因此,如果函数返回的是局部变量的值,不涉及地址,程序不会出错。但是如果返回的是局部变量的地址(指针)的话,程序运行后会出错。因为函数只是把指针复制后返回了,但是指针指向的内容已经被释放了,这样指针指向的内容就是不可预料的内容,调用就会出错。
准确的来说,函数不能通过返回指向栈内存的指针(注意这里指的是栈,返回指向堆内存的指针是可以的)。
stat命令用于显示文件的状态信息。stat命令的输出信息比 ls 命令的输出信息要更详细。
创建用户命令两条:
adduser
useradd
用户删除命令:
两个用户创建命令之间的区别:
adduser: 会自动为创建的用户指定主目录、系统shell版本,会在创建时输入用户密码。
useradd:需要使用参数选项指定上述基本设置,如果不使用任何参数,则创建的用户无密码、无主目录、没有指定shell版本。
1 | 使用 adduser |
因此 adduser 常用参数选项为:
--home
: 指定创建主目录的路径,默认是在/home目录下创建用户名同名的目录,这里可以指定;如果主目录同名目录存在,则不再创建,仅在登录时进入主目录。
--quiet
: 即只打印警告和错误信息,忽略其他信息。
--debug
: 定位错误信息。
--conf
: 在创建用户时使用指定的configuration文件。
--force-badname
: 默认在创建用户时会进行/etc/adduser.conf中的正则表达式检查用户名是否合法,如果想使用弱检查,则使用这个选项,如果不想检查,可以将/etc/adduser.conf中相关选项屏蔽。
1 | 使用 useradd |
为用户指定参数的 useradd 命令,常用命令行选项:
-d
: 指定用户的主目录
-m
: 如果存在不再创建,但是此目录并不属于新创建用户;如果主目录不存在,则强制创建; -m和-d一块使用。
-s
: 指定用户登录时的shell版本
-M
: 不创建主目录
1 | 解释: |
删除用户命令
userdel
只删除用户:
sudo userdel 用户名
连同用户主目录一块删除:
sudo userdel -r 用户名
相关文件:
为组用户增加 root 权限:
1 | 修改 /etc/sudoers 文件,找到下面一行,在 root 下面添加一行,如下所示: |
硬链接
硬链接说白了是一个指针,指向文件索引节点,系统并不为它重新分配inode。可以用: ln 命令来建立硬链接。
尽管硬链接节省空间,也是Linux系统整合文件系统的传统方式,但是存在一下不足之处:
(1)不可以在不同文件系统的文件间建立链接
(2)只有超级用户才可以为目录创建硬链接。
软链接(符号链接)
软链接克服了硬链接的不足,没有任何文件系统的限制,任何用户可以创建指向目录的符号链接。因而现在更为广泛使用,它具有更大的灵活性,甚至可以跨越不同机器、不同网络对文件进行链接。
建立软链接,只要在 ln 后面加上选项 –s
1 | ln -s abc cde # 建立 abc 的软连接 |
删除链接
1 | rm -rf symbolic_name |
1 | 列出当前目录及子目录下所有文件和文件夹 |
1 | grep 内容过滤 |
1 | xargs |
设置 ~/.bashrc
添加 set -o vi – 可以直接使用 vim 的各种快捷键
VIM 快捷键:
gcc 工作流程
1 | 预处理 头文件展开 宏替换 |
gcc 参数
1 | 指定编译输出的名字 |
静态库制作和使用
1 | 步骤 |
动态库制作和使用
1 | 步骤 |
makefile 的三要素:
写法:
1 | app: main.c add.c sub.c div.c mul.c |
如果更改其中一个文件,所有的源码都重新编译
可以考虑编译过程分解,先生成 .o 文件,然后使用 .o 文件编程结果
规则是递归的,依赖文件如果比目标文件新,则重新生成目标文件
1 | ObjFiles=main.o add.o sub.o div.o mul.o |
makefile 的隐含规则:默认处理第一个目标
1 | get all .c files |
makefile 变量:
1 | get all .c files |
make -f makefile1 指定makefile文件进行编译
1 | SrcFiles=$(wildcard *.c) |
gdb 调试入门,大牛写的高质量指南
gdb 调试利器
启动gdb:gdb app
在gdb启动程序:
gdb跟踪core
设置生成 core :ulimit -c unlimited
取消生成 core: ulimit -c 0
设置 core 文件格式:/proc/sys/kernel/core_pattern
文件不能 vi,可以用后面的套路:echo “/corefile/core-%e-%p-%t” > core_pattern
core 文件如何使用:
gdb app core
如果看不到在哪儿core 可以用 where 查看在哪儿产生的 core
ulimit -a 查看所有资源的上限
env 查看环境变量
echo $PATH 打印指定的环境变量
char *getenv()
获取环境变量
创建一个进程:
pid_t fork(void)
返回值:
获得pid,进程 id,获得当前进程
pid_t getpid(void)
获得当前进程父进程的 id
pid_t getppid(void)
ps ajx 查看父进程和子进程相关信息
进程共享:
父子进程之间在fork后,有哪些相同和不同:
似乎,子进程复制了父进程 0-3G 用户空间内容,以及父进程的 PCB, 但 pid 不同。真的每 fork 一个子进程都要将父进程的 0-3G 地址空间完全拷贝一份,然后在映射至屋里内存吗?当然不是,父子进程间遵循读时共享写时复制。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
孤儿进程与僵尸进程:
回收子进程,知道子进程的死亡原因,作用:
pid_t wait(int *status)
子进程的死亡原因:
pid_t waitpid(pid_t pid, int *status, int options)
< -1
组ID-1
回收任意0
回收和调用进程组 ID 相同组内的子进程>0
回收指定的 pidIPC : 进程间通信,通过内核提供的缓存区进行数据交换的机制
IPC 通信的方式有几种:
读管道:
写管道:
管道缓冲区大小
可以使用 ulimit –a
命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。通常为:
1 | pipe size (512 bytes, -p) 8 |
也可以使用 fpathconf
函数,借助参数选项来查看。使用该宏应引入头文件<unistd.h>
long fpathconf(int fd, int name);
管道的优劣
优点:
缺点:
FIFO通信
FIFO 有名管道,实现无血缘关系进程通信
int mkfifo(const char *pathname, mode_t mode);
mmap映射共享区
1 | void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset); |
返回:
参数:
释放映射区
1 | int munmap(void *addr, size_t length); |
匿名映射
通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。 可以直接使用匿名映射来代替。其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags来指定。
使用 MAP_ANONYMOUS
(或 MAP_ANON
), 如:
1 | int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); |
需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
1 | /* |
信号的概念
信号的特点
信号的机制
信号的产生
信号的状态
信号的默认处理方式
信号的 4 要素
创建一个会话需要注意以下 5 点注意事项:
守护进程:
Daemon 进程,是 Linux 中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。
创建守护进程,最关键的一步是调用 setsid 函数创建一个新的 session 。并成为 session leader。
创建守护进程模型:
会话:进程组的更高一级,多个进程组对应一个会话
进程组:多个进程在同一个组,第一个进程默认是进程组的组长
创建会话的时候,组长不可以创建,必须是组员创建。
创建会话的步骤:创建子进程,父进程终止,子进程当会长
守护进程的步骤:
- 创建子进程 fork
- 父进程退出
- 子进程当会长 setsid
- 切换工作目录 $HOME
- 设置掩码 umask
- 关闭文件描述符,为了避免浪费资源
- 执行核心逻辑
- 退出
1 | int pthread_create(pthread_t *thread, const pthread_attr_t *attr, |
1 |
|
扩展了解:
通过 nohup 指令也可以达到守护进程创建的效果
nohup cmd [> 1.log] &
线程是最小的执行单位,进程是最小的系统资源分配单位
查看 LWP 号:ps -Lf pid
查看指定线程的 lwp 号
线程非共享资源
线程优缺点:
1 | alias echomake=`cat ~/bin/makefile.template >> makefile` |
线程退出注意事项:
线程回收函数:
1 | int pthread_join(pthread_t thread, void **retval); |
杀死线程:
1 | int pthread_cancel(pthread_t thread); |
被pthread_cancel 杀死的线程,退出状态为 PTHREAD_CANCELED
线程分离:
1 | int pthread_detach(pthread_t thread); |
此时不需要 pthread_join回收资源
线程 ID 在进程内部是唯一的
进程属性控制:
初始化线程属性
1 | int pthread_attr_init(pthread_attr_t *attr); |
销毁线程属性
1 | int pthread_attr_destroy(pthread_attr_t *attr); |
设置属性分离态
1 | int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); |
查看线程库版本:
1 | getconf GNU_LIBPTHREAD_VERSION |
创建多少个线程?
线程同步:
解决同步的问题:加锁
mutex 互斥量:
1 | pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;// 常量初始化,此时可以使用init |
读写锁的特点:读共享,写独占,写优先级高
读写说任然是一把锁,有不同状态:
1 | int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, |
条件变量(生产者消费者模型):
1 | pthread_cond_t cond = PTHREAD_COND_INITIALIZER; |
信号量 加强版的互斥锁:
信号量是进化版的互斥量,允许多个线程访问共享资源
1 |
|
文件锁:
1 |
|