cherish

返朴归真


  • Home

  • Archives

  • Tags

  • Categories

分布式协调服务zookeeper简论

Posted on 2019-08-20 | In 中间件 , ZooKeeper |
Words count in article: 5.5k | Reading time ≈ 19

ps.本文为《从Paxos到Zookeeper 分布式一致性原理与实践》笔记之一

1 ZooKeeper

  • ZooKeeper曾是Apache Hadoop的一个子项目,是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、master选举、分布式锁和分布式队列等。

  • ZooKeeper是Google的Chubby一个开源的实现,由雅虎创建,是Hadoop和Hbase的重要组件。

  • ZooKeeper没有直接采用paxos算法,而是采用了一种被称为ZAB(Zookeeper Atomic Broadcast)的一致性协议

  • ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

1.1 ZooKeeper可以保证如下分布式一致性特性

  1. 顺序一致性:从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到Zookeeper中;

  2. 原子性:所有事务的请求结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么在整个集群中所有机器上都成功应用了某一个事务,要么都没有应用,没有中间状态;

  3. 单一视图:无论客户端连接的是哪个Zookeeper服务器,其看到的服务端数据模型都是一致的。

  4. 可靠性:一旦服务端成功应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。

  5. 实时性:Zookeeper仅仅保证在一定的时间内,客户端最终一定能够从服务端上读到最终的数据状态。

1.2 ZooKeeper的四个设计目标

zk致力于提供一个高性能、高可用、且具有严格的顺序访问控制能力(主要是写操作)的分布式协议,最后者能使得zk能实现一些复杂的同步语义。

  1. 简单的数据模型:能够通过一个共享的、树型结构的名字空间来进行相互协调。这里是树形结构的名字空间是指zk服务器内存中的一个数据结构,由其一系列的ZNode数据节点组成,他们的层级关系就像文件系统的目录结构,不过zk将其全量数据存储在内存中,以达到高吞吐。

  2. 可以构建集群:zk集群通常由一组机器组成,集群中的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都保持着通信。集群中只要超过一半的机器可以正常工作,zk就可以对外提供服务。zk客户端会选择和集群中任意一台机器共同来创建一个tcp链接,如果连接断开,客户端会自动连接到服务机器的其他机器。

  3. 顺序访问:对于来自客户端的每个更新请求,Zookeeper都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序。

  4. 高性能:Zookeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,因此它尤其适用于以读操作为主的应用场景。

2 ZooKeeper的基本概念

2.1 集群角色

  • 最典型的集群就是Master/Slave模式(主备模式),此情况下把所有能够处理写操作的机器称为Master机器,把所有通过异步复制方式获取最新数据,并提供读服务的机器为Slave机器。
  • Zookeeper不采用主备模式,它引入了Leader、Follower、Observer三种角色,Zookeeper集群中的所有机器通过Leaser选举过程来选定一台被称为Leader的机器,Leader服务器为客户端提供读和写服务,Follower和Observer提供读服务,但是Observer不参与Leader选举过程,不参与写操作的”过半写成功”策略,Observer可以在不影响写性能的情况下提升集群的性能。

leader: 是整个集群工作机制中的核心,其主要工作有:

  1. 事务请求的唯一调度和处理者,保证集群事务处理的顺序性。
  2. 集群内部各服务器的调度者。

follower:是zookeeper集群状态的跟随者,其主要工作是:

  1. 处理客户端的非事务请求,转发事务请求给leader服务器。
  2. 参与事务请求proposal的投票
  3. 参与leader选举投票

observer:和follower唯一的区别在于,observer服务器只提供非事务服务,不参与任何形式的投票,包括事务请求proposal的投票和leader选举投票。

  1. 通常在不影响集群事务处理能力的前提下提升集群的非事务处理能力。

2.2 会话

  • 指客户端会话,一个客户端连接是指客户端和服务端之间的一个TCP长连接,Zookeeper对外的服务端口默认为2181,客户端启动的时候,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来自服务器的Watch事件通知。

2.3 数据节点

  • 我们常说的节点指的是集群中的机器节点,zk中节点有两类,第一类指构成集群的机器,称为机器节点,第二类是指数据模型中的数据单元,称为数据节点-Znode,Zookeeper将所有数据存储在内存中,数据模型是一棵树,由斜杠/进行分割的路径,就是一个ZNode,如/foo/path1,每个ZNode都会保存自己的数据内存,同时还会保存一些列属性信息。
  • ZNode分为持久节点和临时节点两类,持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上,而临时节点的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。
  • 另外,Zookeeper还允许用户为每个节点添加一个特殊的属性:SEQUENTIAL。一旦节点被标记上这个属性,那么在这个节点被创建的时候,Zookeeper会自动在其节点后面追加一个整形数字,其是由父节点维护的自增数字。
  • 临时,持久和顺序,如此,数据节点一共有四种类型:
    1. 持久节点(PERSISTENT)
      • zk中最常见的一种节点类型。除非主动删除,否则一直保留
    2. 持久顺序节点(PERSISTENT_SEQUENTIAL)
      • 基本和持久节点一致,额外的特性表现在顺序性上。持久顺序节点在创建节点的时候,zk会自动给它的名字加上数字后缀,表示在该父节点下的创建顺序,后缀上线是整型最大值。
    3. 临时节点(EPHEMERAL)
      • 临时节点的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。
    4. 临时顺序节点(EPHEMERAL_SEQUENTIAL)
      • 同临时节点,再加上顺序特性。

        2.4 stat

  • 数据节点中除了有数据内容外,还有一个stat对象来记录节点的状态信息:

    2.5 版本

  • 对于每个ZNode,Zookeeper都会为其维护一个叫作Stat的数据结构,Stat记录了这个ZNode的三个数据版本,分别是version(当前ZNode的版本)、cversion(当前ZNode子节点的版本)、aversion(当前ZNode的ACL版本)。
  • 类似于乐观锁和cas,保证原子性操作

    2.6 事务操作

  • 在ZooKeeper中,能改变ZooKeeper服务器状态的操作称为事务操作。一般包括数据节点创建与删除、数据内容更新和客户端会话创建与失效等操作。对应每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID,用ZXID表示,通常是一个64位的数字。每一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些事务操作请求的全局顺序。

    2.7 watcher

  • watcher是事件监听器,Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端。该机制是zk实现分布式协调服务的重要特性。
  • 其逻辑如下图:
  • 当客户端向服务端注册watcher时,也会将watcher对象存储在客户端的watcherManager中,当服务端出发watcher时间后,向客户端发送通知,客户端从watcherManager中去处watcher对象来执行回调逻辑。

    2.8 ACL

    Zookeeper采用ACL(Access Control Lists)策略来进行权限控制,类似于unix文件系统的权限控制,
  • 权限模式
    • ip模式
      • 通过ip地址粒度来进行权限控制。
    • digest
      • 类似于username:password形式配置权限标识来控制,
    • world
      • 即任何人都可以访问
    • super
      • 一种特殊的digest模式,只不过权限是超级用户。
  • 权限:
    • CREATE:创建子节点的权限。
    • READ:获取节点数据和子节点列表的权限。
    • WRITE:更新节点数据的权限。
    • DELETE:删除子节点的权限。
    • ADMIN:设置节点ACL的权限。
  • 值得注意的是,CREATE和READ都是针对子节点的权限控制。

3 ZAB协议

见文章《ZAB协议分析》一文

4 Zookeeper应用场景

4.1 数据发布/订阅

  • 发布订阅系统,即所谓的配置中心,发布者将数据发布到zk的一个或者一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的。

  • 常规的发布订阅系统有push和pull两种模式,在push模式中,服务端主动将数据更新发送给所有订阅的客户端。pull模式中,客户端通常采用盯视轮询拉取的方式发送请求来服务端获取。

  • zk的发布订阅系统采用推拉结合的模式,客户端向服务端祖册自己需要关注的数据节点,一旦该节点的数据(也就是配置信息)发生变更,服务端会发送watcher时间通知客户端来获取数据。

4.2 负载均衡

  • 这里说的负载均衡是指软负载均衡。在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就须要在这些对等的服务器中选择一个来执行相关的业务逻辑,其中比较典型的是消息中间件中的生产者,消费者负载均衡。
  • 消息中间件中发布者和订阅者的负载均衡,linkedin开源的KafkaMQ和阿里开源的metaq都是通过zookeeper来做到生产者、消费者的负载均衡。我们以kafka为例子。

    4.2.1 kafka概念:

  • 消息生产者:producer
  • 消息消费者:consumer
  • 主题:即Topic,由用户定义并配置在Kafka服务端,用于建立生产者和消费者之间的订阅关系:生产者发送消息到指定Topic下,消费者从这个Topic下消费消息。
  • Broker:即Kafka服务器,用于存储消息,在消息中间件中通常被称为Broker
    消费者分组:Group
  • Offset:消息存储在Kafka的Broker上,消费者拉取消息数据的过程中需要知道消息在文件中的偏移量,这个偏移量就是所谓的Offset
  • ZooKeeper负责管理所有Broker服务器列表,并且建立了对应路径来对其进行管理/brokers/ids
    每个Broker服务器在启动时,都会到ZooKeeper上进行注册,其节点路径为/broker/ids/[0…N]
  • Topic注册:Kafka当中,会将同一个Topic的消息分成多个区,分布到多个Broker上,这些分区信息和Broker的对应关系由ZooKeeper来维护

    4.2.2 ZooKeeper负载均衡实现

  • 每当一个Broker启动时,会首先完成Broker注册过程,在ZooKeeper的节点列表里保存Broker。
  • Kafka的生产者会对ZooKeeper上的“Broker的新增与减少”、“Topic的新增和减少”和“Broker和Topic关联关系的变化”等事件注册Watcher监听
  • 通过ZooKeeper的Watcher通知能够让生产者动态的获取Broker和Topic的变化情况
  • Kafka有消费者分组的概念,每个消费者分组包含了若干个消费者,每一条消息只会发送给分组内的一个消费者,不同消费者分组消费自己特定的Topic下面的消息,互不干扰
  • Kafka会为每个消费者分配全局唯一的Consumer ID,采用“Hostname:UUID”形式来表示
  • 每个消费者一旦确定了对一个消息分区的消费权利,ZooKeeper会将其Consumer ID写入到对应消息分区的临时节点上
  • 消费进度管理:Kafka需要定时地将分区消息的消费进度,即Offset记录到ZooKeeper上去

4.3 命名服务

  • 命名服务也是分布式系统中比较常见的一类场景。在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。
  • 其中较为常见的就是一些分布式服务框架中的服务地址列表。通过调用ZK提供的创建节点的API,能够很容易创建一个全局唯一的path,这个path就可以作为一个名称。
  • 阿里巴巴集团开源的分布式服务框架Dubbo中使用ZooKeeper来作为其命名服务,维护全局的服务地址列表,在Dubbo实现中
    • 服务提供者在启动的时候,向ZK上的指定节点/dubbo/${serviceName}/providers目录下写入自己的URL地址,这个操作就完成了服务的发布。
    • 服务消费者启动的时候,订阅/dubbo/${serviceName}/providers目录下的提供者URL地址, 并向/dubbo/${serviceName} /consumers目录下写入自己的URL地址。
    • 注意,所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。
    • 另外,Dubbo还有针对服务粒度的监控,方法是订阅/dubbo/${serviceName}目录下所有提供者和消费者的信息。

      4.4 分布式通知/协调

  • ZooKeeper中特有watcher注册与异步通知机制,能够很好的实现分布式环境下不同系统之间的通知与协调,实现对数据变更的实时处理。使用方法通常是不同系统都对ZK上同一个znode进行注册,监听znode的变化(包括znode本身内容及子节点的),其中一个系统update了znode,那么另一个系统能够收到通知,并作出相应处理。
    • 心跳检测式通知/协调:检测系统和被检测系统之间并不直接关联起来,而是通过zk上某个节点关联,比如两个客户端都在一个节点下创建各自的临时节点,并定时心跳去检测对方节点的存在,达到心跳检测的目的。
    • 系统调度式通知/协调:有控制台和推送系统两部分组成,控制台的职责是控制推送系统进行相应的推送工作。管理人员在控制台作的一些操作,实际上是修改了ZK上某些节点的状态,而ZK就把这些变化通知给他们注册Watcher的客户端,即推送系统,于是,作出相应的推送任务。
    • 工作汇报式通知/协调:一些类似于任务分发系统,子任务启动后,到zk来注册一个临时节点,并且定时将自己的进度进行汇报(将进度写回这个临时节点),这样任务管理者就能够实时知道任务进度。
  • 总之,使用zookeeper来进行分布式通知和协调能够大大降低系统之间的耦合

4.5 集群管理

  • 通常用于那种对集群中机器状态,机器在线率有较高要求的场景,能够快速对集群中机器变化作出响应。这样的场景中,往往有一个监控系统,实时检测集群机器是否存活。过去的做法通常是:监控系统通过某种手段(比如ping)定时检测每个机器,或者每个机器自己定时向监控系统汇报“我还活着”。 这种做法可行,但是存在两个比较明显的问题:
    • 集群中机器有变动的时候,牵连修改的东西比较多。
    • 有一定的延时。
  • 利用ZooKeeper有两个特性,就可以实时另一种集群机器存活性监控系统:
    • 客户端在节点x上注册一个Watcher,那么如果 x的子节点变化了,会通知该客户端。
    • 创建EPHEMERAL类型的节点,一旦客户端和服务器的会话结束或过期,那么该节点就会消失。
  • 例如,监控系统在 /clusterServers 节点上注册一个Watcher,以后每动态加机器,那么就往 /clusterServers 下创建一个 EPHEMERAL类型的节点:/clusterServers/{hostname}. 这样,监控系统就能够实时知道机器的增减情况,至于后续处理就是监控系统的业务了。

    4.6 Master选举

  • 在分布式环境中,相同的业务应用分布在不同的机器上,有些业务逻辑(例如一些耗时的计算,网络I/O处理),往往只需要让整个集群中的某一台机器进行执行,其余机器可以共享这个结果,这样可以大大减少重复劳动,提高性能,于是这个master选举便是这种场景下的碰到的主要问题。
  • 利用ZooKeeper的强一致性,能够保证在分布式高并发情况下节点创建的全局唯一性,即:同时有多个客户端请求创建同名的如名字为 /currentMaster的节点,最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很轻易的在分布式环境中进行集群选取了。

    4.6.1 动态master选举

  • 上述场景演化一下,就是动态Master选举。这就要用到EPHEMERAL_SEQUENTIAL类型节点的特性了。上文中提到,所有客户端创建请求,最终只有一个能够创建成功。在这里稍微变化下,创建EPHEMERAL_SEQUENTIAL节点,于是所有的请求最终在ZK上创建结果的一种可能情况是这样
    • /currentMaster/{sessionId}-1 ,/currentMaster/{sessionId}-2 ,/currentMaster/{sessionId}-3
  • 每次选取序列号最小的那个机器作为Master,如果这个机器挂了,由于他创建的节点会马上消失,那么之后最小的那个机器就是Master了。

4.7 分布式锁

  • 分布式锁,这个主要得益于ZooKeeper为我们保证了数据的强一致性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
    • 所谓保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。
    • 控制时序,就是所有试图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock 已经预先存在,客户端在它下面创建临时有序节点(这个可以通过节点的属性控制:CreateMode.EPHEMERAL_SEQUENTIAL来指定)。Zk的父节点(/distribute_lock)维持一份sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。

      4.8 分布式队列

  • 队列方面,简单地讲有两种,一种是常规的先进先出队列,另一种是要等到队列成员聚齐之后的才统一按序执行。对于第一种先进先出队列,和分布式锁服务中的控制时序场景基本原理一致,这里不再赘述。
  • 第二种队列其实是在FIFO队列的基础上作了一个增强。通常可以在 /queue 这个znode下预先建立一个/queue/num 节点,并且赋值为n(或者直接给/queue赋值n),表示队列大小,之后每次有队列成员加入后,就判断下是否已经到达队列大小,决定是否可以开始执行了。
  • 这种用法的典型场景是,分布式环境中,一个大任务Task A,需要在很多子任务完成(或条件就绪)情况下才能进行。这个时候,凡是其中一个子任务完成(就绪),那么就去 /taskList 下建立自己的临时时序节点(CreateMode.EPHEMERAL_SEQUENTIAL),当 /taskList 发现自己下面的子节点满足指定个数,就可以进行下一步按序进行处理了。

paxos算法论述和推导

Posted on 2019-08-12 | In 分布式算法&理论 , 分布式事务和数据一致性 |
Words count in article: 4.2k | Reading time ≈ 14

前言

脑裂

  • 在HA集群系统中,假设有同一个整体、动作协调的节点A 和节点B,节点A和B之间通过heartBeat来检查对方的存活状态,负责协调保证整个集群服务的可用性。正常情况下,如果节点A通过心跳检测不到B的存在的时候,就会接管B的资源,同理节点B检查不到B的存活状态的时候也会接管A的资源。

  • 如果出现网络故障,就会导致A和B同时检查不到对方的存活状态认为对方出现异常,这个时候就会导致A接管B的资源,B也会接管A的资源。原来被一个节点访问的资源就会出现被多个节点同时访问的情况,这种情况就是脑裂现象。

  • 嗯,这是比较官方的解释,说人话的话,其实就是在一个集群中,有多个“大脑”,即多个master

    三态

  • 在传统的单机系统中,我们调用一个函数,这个函数要么返回成功,要么返回失败,其结果是确定的。可以概括为传统的单机系统调用只存在两态(2-state system):成功和失败。

  • 然而在分布式系统中,由于系统是分布在不同的机器上,系统之间的请求就相对于单机模式来说复杂度较高了。具体的,节点 A 上的系统通过 RPC (Remote Procedure Call) 方式与节点 B 上的系统进行通信,在这个请求结果存在三态(3-state system):也就是成功、失败和超时,不要小瞧超时这个状态,因为它几乎是所有分布式系统复杂性的根源。

1 paxos算法

  • Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。- - Paxos算法及变种算法在分布式系统中应用广泛。基于Paxos算法的变种有:ZAB、Raft。 Zookeeper 中的ZAB协议也是Paxos算法的变种。
  • Paxos是一个解决共识问题consensus problem的算法,现实中Paxos的实现以及成为一些世界级软件的心脏,如Cassandra, Google的Spanner数据库, 分布式锁服务Chubby。一个被Paxos管理的系统实际上谈论的是值状态和跟踪等问题,其目标是建造更高可用性和强一致性的分布式系统。
  • Paxos算法解决的问题是在一个可能发生消息可能会延迟、丢失、重复的分布式系统中如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。这 个“值”可能是一个数据的某,也可能是一条log日志等;根据不同的应用环境这个“值”也不同。

    1.1 角色

  • Paxos中有三类角色Proposer、Acceptor及Learner,主要交互过程在Proposer和Acceptor之间。网络中的任意节点,都可能既是Proposer,又是Acceptor。
  • 当某个节点想将分布式系统中进行写操作的,它就是Proposer。除Proposer外,所有接收到请求,被要求对某个值进行更改的其他节点,就是Acceptor。

1.2 比喻推导前提

  • 举个例子,一个城市为了确定城市未来的发展方针,市长皮皮虾决定集思广益,向全体市民寻求建议。每个人都有建议权,也都有投票权(一个市民在发展方针的时候,他就是proposer,其他人是acceptor,此时,发展方针即表示节点希望推动的值变更或者说值状态,他希望所有节点都接受自己提出的值变更)

  • 在现实环境中我们可以在一个大会堂召集全体市民共同讨论或在微信群中讨论(基于内存共享方式);但在基于消息传递的分布式环境中每个人只能通过手机短信与其它人沟通。我们要寻求一种机制以解决如何在这种会延迟、丢失的环境中确定一个城市的发展方针。

  • 在讨论之前,我们得制定一些投票规则,以便能够胜利选出呼声最高的发展方针。

    1. 最终只能确定一个建议
    2. 少数服从多数。只要建议被多数人同意即可确定该建议。
  • 为了保证投票环节让所有人信服,我们得保证如下几点:

    1. 只有被提出来的建议才能被大家投票同意。
      • 也就是大家只能投票给被建议出来的选项。
    2. 最终一定要能够得到一个被大多数人同意的建议。
    3. 不同人的提议可以重复。
    4. 如果某个人认为大家同意了某个建议,那么这个建议必须真的是被大家同意的。
      • 也就是要让所有市民心服口服,人们认为的众望所归的建议,必须真的是大家众望所归的建议。

1.3 比喻推导过程

为了制定行之有效的机制,市长皮皮虾召集了一大批专家团队来规划整个决策机制,在讨论当中,专家们提出了许多未来可能遇到的问题,并尝试完善规则:

1.3.1 情况一

如果因为宣传不充分,很多人准备也不到位,结果只有一位市民提出了自己的建议,它希望这个小城大力发展纸尿裤行业,希望大家在即将到来的投票中选择自己的提议。这个明显是帮宝适董事长提的傻逼建议,在初步征询阶段就得不到市民的认可,于是情况陷入了僵局,在没有新的建议出来的情况下,大家都不同意,导致决策机制彻底失灵。

解决办法:为了使得这个表决机制不会陷入这个局面,一根筋的市长皮皮虾决定增加一条硬性规定,我们称他为P1:不许不投票,每一个人必须同意他收到的第一个建议。这样即使只有一个建议被提出,也能够被采纳。

1.3.2 情况二

此时一位专家说:基于情况一,我们要求每一个人必须同意他收到的第一个提议,但因为我们又需要一个提议必须被大多数人同意才能生效,所以一个人必须可以同时同意多个人的建议,即一个人必须可以拥有多次投票权可以投给不同的人,不然发生A同意B,B同意C,C同意D,D同意A的情况,永远也得不出一个超过半数同意的提议。

市长皮皮虾表示这位专家说的很有道理。

但这时又有一位专家提醒,就算我们允许一个人可以投给不同的人,也会遇到类似的僵局,比如A同意B和C的提议,B同意C和D的提议,C同意D和A的提议,D同意A和B的提议,这样每个人的提议都有两票,也会陷入僵局。

要避免出现这种问题,就要求被某个人投票的这些建议者们,他们的提议内容都是一样的。

于是皮皮虾市长规定,我们称他为P2:市民只能有一个立场,抱定这个立场后,可以投票给不同的但都是提议该立场的建议者,以便选出最后得票最高的建议。

1.3.3 情况三

此时一位专家说:如果有五个人甲乙丙丁戊,甲乙丙三个人同意了方案A,方案A成为了主流意见。丁,戊 2人由于之前手机没电,没收到通知,所以他们下线了。当他们2人开机后(注意此时甲乙丙之间的投票已经结束,方案A成为了主流意见),丁和戊以为主流意见尚未选出,刚好丁也有提议没发出去,所以丁给戊发短信提议了方案B,这个提议是戊开机后第一次收到的提议,根据P1原则,他必须同意他接收到的第一个提议,并将其作为自己的立场,所以戊同意方案B。但这样就会导致戊与甲乙丙他们意见不一致。

一个人意见不一致还好,但因为有这样的漏洞,所以可能最后和主流意见不一致的人会非常多,这不利于我们构建和谐且令人心服的讨论。

于是市长皮皮虾对P2进行了补充,我们称为P2a:一旦一个提议成为了主流的意见,那么之后的人再次投票的提议内容,必须和主流意见内容一致。

也就是说,戊在开机后同意的第一个提议必须是方案A才不会出现信息不一致的现象。但戊开机后必须得接受第一个提议(P1原则),并且无法干涉提议中的内容(方案A还是方案B)。所以最好的办法通过某种方式让丁的提议中的内容与甲乙丙的提议相同。这样戊同意的第一个提议就是方案A

于是皮皮虾再次对P2a进行补充,我们称为P2b:一旦一个提议成为主流意见,那么之后的人再次提议,提议中的内容必须跟主流意见一致。

如何让刚开机的戊提议的内容必须与甲乙丙同意的一致呢?要想达到这一点,只要达到另一个条件即可:

我们称为P2c:存在一个提议A,对于大多数市民来说,要么他们中所有人在同意A前没有同意其他提议,要么他们已经同意的所有提议,内容都和A一模一样。

如果能做到P2c这一点,那么P2b自然就达到了,P2c是P2b的充分条件。也是P2的充分条件。

1.4 paxos流程比喻

也就是说,要想这个表决机制表决出的结果是真的所有人都认同的,那么必须同时满足P1,P2c(P2的充分条件),那如何设计表决的程序,才能使表决满足P1和P2c呢?

而且P2c条件中,有提议的时间先后概念,在一个只能依靠短信沟通的环境中,如何确定一个提议早于另外一个提议被提出呢?毕竟就算某个提议A比提议B早提出,它也不可能永远比B早到达每一个节点。众人觉得应该引入一个全局的编号,每个提议对应一个编号,用这个编号来表示时间维度的早和晚。但具体如何实施呢?

在众人一筹莫展的时候,一个天才专家站了出来,它设计了一个流程,能够使P1和P2c成立。

专家建议,谁的提议最后被选为了最终提议,可以奖励一个巨量的奖金,这样,每一个提议者都会竭尽所能的让自己的提议成为最终提议(模拟真实paxos实现中proposer始终试图让acceptor接受自己提议的场景)

专家还建议,可以引入贿选机制,每个提议者都可以为自己的提议选定一个贿选金额(模拟实际情况中的全局编号),承诺如果自己的提议成为最终提议,每个接受自己的提议的投票人可以得到该金额。贿选金额维护在一个地方,每个提议者在提议之前都知道目前最高的贿选金额是多少了。

为了得到最后的奖金,每个提议者都会一直提高这个贿选金额(这就模拟了实际中一直自增的全局编号,也合理了为什么accept会投票给后来的提议,因为后来的提议给的钱多啊)。

基于上面两个前提,专家提出了最后的流程,这个表决要分两步:

  • 准备阶段,大家先内部讨论一下,不急着投票
    1. proposer选择一个贿选金额,并将信息以
      [贿选金额,提议内容]
      的kv形式广播(prepare请求)。
    2. 如果收信人收到 [贿选金额,提议内容] 后。
      • 如果该提议的金额大于它已经回复的所有提议的金额。那么acceptor将会为了钱出卖自己的上家,将自己上次接受的提议内容以及上家给了他多少钱回复给proposer,即[之前最高的贿选金额,我之前同意的提案内容],并承诺不再接受小于这个金额的提议。
      • 如果该提议的金额不大于它已经接受的那个提议的金额。那么直接无视。(贿选金额没有吸引力,无视)
  • 同意阶段,经过阶段一,proposer们已经通过acceptor们的反馈知道了每个acceptor的投票承诺。(因为prepare是广播形式,所以会受到全部acceptor的返回,即要么得到接受提议的回馈,表示我接受你的提案了,并且将我上家的提案告诉你。要么我就无视你,无视也是一种表态),
    1. 当proposer收到多数人接受提议的反馈信息后,就进入同意阶段(重点,收到大多数的承诺后才会进入同意阶段)。它要向反馈给它信息的人再次发送一个请同意该提议的请求(accept请求),内容为
      [第一步的那个贿选金额,收到的反馈中价码最高的那个提议内容]
      (如果proposer接收到的所有回复中都没有acceptor已经接受过的提议内容,则proposer可以自由决定提议内容)
    注意,对于某些proposer来说,这里再次发出的提议内容,可能已经和自己之前在第一步的提议不一样了。因为如果他通过acceptor们的反馈(里面包含acceptor们接受的提议),知道了自己之前的提议没有成为主流意见,大伙大多数都同意了另一个给钱最壕的proposer的提议,那么为了彰显自己的正确,它也默默的把提议内容改成了大家都同意的提议内容。
    1. acceptor接收到请求:
      • 在不违背向其它人承诺的前提下,收到该提议请求后会立即同意该请求,并回馈一个接受的消息。(即一个acceptor只要尚未响应过任何编号大于N的prepare请求,那么它就可以接受这个编号为N的accept请求)
      • 而如果此时acceptor又叛变了,它接受了一个金额更高(编号更高)的提议,那么,他会无视这个accept请求。

如果一个proposer发出的accept请求没有得到大多数的回应,那么他就会知道大多数acceptor已经叛变了,无奈之下,他只能重新进入准备阶段,重新选择一个更大的金额(编号n),再去冲击一波。(此时他的提案,就是之前他默默改成的大家都同意的提议)

否则,如果一个proposer发出的accept请求得到了大多数的回应,那么流程结束,该proposer的提案胜出,至于算法如何将胜出的提案通知给 Learner,有如下三种方案,各有优劣

1.5 Learner学习被选定的value

Learner学习(获取)被选定的value有如下三种方案:

1.6 后述

Leslie Lamport没有用数学描述Paxos,但是他用英文阐述得很清晰。至于Paxos中一直提到的一个全局唯一且递增的proposer number,其如何实现,引用如下

如何产生唯一的编号呢?在《Paxos made simple》中提到的是让所有的Proposer都从不相交的数据集合中进行选择,例如系统有5个Proposer,则可为每一个Proposer分配一个标识j(0~4),则每一个proposer每次提出决议的编号可以为5*i + j(i可以用来表示提出议案的次数)

除此之外,如何保证paxos的活性

通过选取主Proposer,就可以保证Paxos算法的活性。至此,我们得到一个既能保证安全性,又能保证活性的分布式一致性算法——Paxos算法。

常用shell命令导航

Posted on 2019-07-21 | In 计算机协议和技术 , Linux相关 |
Words count in article: 25.4k | Reading time ≈ 102

导航栏位于右侧,只收录常用命令,按字母表排序

A

alias-设置指令的别名

  • 简介:

  • 别名就是一种便捷方式,可以为用户省去输入一长串命令序列的麻烦。Linux alias命令用于设置指令的别名。用户可利用alias,自定指令的别名。若仅输入alias,则可列出目前所有的别名设置。alias的效力仅及于该次登入的操作。若要每次登入是即自动设好别名,可在.profile或.bashrc中设定指令的别名。

  • 语法:

  • alias[别名]=[指令名称]

  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //为apt-get install创建别名。
    $ alias install='sudo apt-get install'

    //alias命令的效果只是暂时的。一旦关闭当前终端,所有设置过的别名就失效了。
    //为了使别名在所有的shell中都可用,可以将其定义放入~/.bashrc文件中。
    //每当一个新的交互式shell进程生成时,都会执行 ~/.bashrc中的命令。
    $ echo 'alias cmd="command seq"' >> ~/.bashrc

    //我们可以创建一个别名rm,它能够删除原始文件,同时在backup目录中保留副本。
    $ alias rm='cp $@ ~/backup && rm $@'
  • 注意事项

    alias命令若不加任何参数,则列出目前所有的别名设置。

    如果需要删除别名,只需将其对应的定义(如果有的话)从~/.bashrc中删除,或者使用unalias命令。也可以使用alias example=,这会取消别名example。

    创建别名时,如果已经有同名的别名存在,那么原有的别名设置将被新的
    设置取代。

    如果我们遇到别名和原生命令同名的情况,我们可以转义要使用的命令,忽略当前定义的别名:
    $ \command
    字符\可以转义命令,从而执行原本的命令。

    在不可信环境下执行特权命令时,在命令前加上\来忽略可能存在的别名总是一种良好的安全实践。这是因为攻击者可能已经将一些别有用心的命令利用别名伪装成了特权命令,借此来盗取用户输入的重要信息。


awk——把文件逐行的读入,以分隔符将其切片,再进行处理。

  • 简介:

    • awk是一个强大的文本分析工具,相对于grep的查找,sed的编辑,awk在其对数据分析并生成报告时,显得尤为强大。简单来说awk就是把文件逐行的读入,以空格为默认分隔符将每行切片,切开的部分再进行各种分析处理。
    • awk有3个不同版本: awk、nawk和gawk,未作特别说明,一般指gawk,gawk 是 AWK 的 GNU 版本。
    • 之所以叫AWK是因为其取了三位创始人 Alfred Aho,Peter Weinberger, 和 Brian Kernighan 的 Family Name 的首字符。
  • 语法:

    • awk [选项参数] '{pattern + action}' {filenames}
    • 尽管操作可能会很复杂,但语法总是这样,其中 pattern 表示 AWK 在数据中查找的内容,而 action 是在找到匹配内容时所执行的一系列命令。花括号({})不需要在程序中始终出现,但它们用于根据特定的模式对一系列指令进行分组。 pattern就是要表示的正则表达式,用斜杠括起来。
    • awk语言的最基本功能是在文件或者字符串中基于指定规则浏览和抽取信息,awk抽取信息后,才能进行其他文本操作。完整的awk脚本通常用来格式化文本文件中的信息。
    • 通常,awk是以文件的一行为处理单位的。awk每接收文件的一行,然后执行相应的命令,来处理文本。
  • 选项值:

    • -F fs or –field-separator fs

      • 指定输入文件折分隔符,fs是一个字符串或者是一个正则表达式。在awk中,文件的每一行中,由域分隔符分开的每一项称为一个域。通常,在不指名-F域分隔符的情况下,默认的域分隔符是空格。
    • -v var=value or –asign var=value

      • 赋值一个用户定义变量。
    • -f scripfile or –file scriptfile

      • 从脚本文件中读取awk命令。
    • -mf nnn and -mr nnn

      • 对nnn值设置内在限制,-mf选项限制分配给nnn的最大块数目;-mr选项限制记录的最大数目。这两个功能是Bell实验室版awk的扩展功能,在标准awk中不适用。
    • -W compact or –compat, -W traditional or –traditional

      • 在兼容模式下运行awk。所以gawk的行为和标准的awk完全一样,所有的awk扩展都被忽略。
    • -W copyleft or –copyleft, -W copyright or –copyright

      • 打印简短的版权信息。
    • -W help or –help, -W usage or –usage

      • 打印全部awk选项和每个选项的简短说明。
    • -W lint or –lint

      • 打印不能向传统unix平台移植的结构的警告。
    • -W lint-old or –lint-old

      • 打印关于不能向传统unix平台移植的结构的警告。
  • 内建变量

    如下变量在awk中具有特定含义。

    • $n 当前记录的第n个字段,字段间由FS分隔
    • $0 完整的输入记录
    • ARGC 命令行参数的数目
    • ARGIND 命令行中当前文件的位置(从0开始算)
    • ARGV 包含命令行参数的数组
    • CONVFMT 数字转换格式(默认值为%.6g)ENVIRON环境变量关联数组
    • ERRNO 最后一个系统错误的描述
    • FIELDWIDTHS 字段宽度列表(用空格键分隔)
    • FILENAME 当前文件名
    • FNR 各文件分别计数的行号
    • IGNORECASE 如果为真,则进行忽略大小写的匹配
    • NF 一条记录的字段的数目
    • NR 已经读出的记录数,就是行号,从1开始
    • OFMT 数字的输出格式(默认值是%.6g)
    • OFS 输出记录分隔符(输出换行符),输出时用指定的符号代替换行符
    • ORS 输出记录分隔符(默认值是一个换行符)
    • RLENGTH 由match函数所匹配的字符串的长度
    • RS 记录分隔符(默认是一个换行符)
    • RSTART 由match函数所匹配的字符串的第一个位置
    • SUBSEP 数组下标分隔符(默认值是/034)
  • 代码示例

    awk工作流程是这样的:读入有’\n’换行符分割的一条记录,然后将记录按指定的域分隔符划分域,填充域,$0则表示所有域,$1表示第一个域,$n表示第n个域。默认域分隔符是”空白键” 或 “[tab]键”,所以$1表示登录用户,$3表示登录用户ip,以此类推。

    • action的使用示例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      //假设last -n 5的输出如下
      $ last -n 5 <==仅取出前五行
      > root pts/1 192.168.1.100 Tue Feb 10 11:21 still logged in
      > root pts/1 192.168.1.100 Tue Feb 10 00:46 - 02:28 (01:41)
      > root pts/1 192.168.1.100 Mon Feb 9 11:41 - 18:30 (06:48)
      > dmtsai pts/1 192.168.1.100 Mon Feb 9 11:41 - 11:41 (00:00)
      > root tty1 Fri Sep 5 14:09 - 14:10 (00:01)

      //使用awk显示最近登录的5个帐号
      $ last -n 5 | awk '{print $1}'
      > root
      > root
      > root
      > dmtsai
      > root

      //如果只是显示/etc/passwd文件中的账户信息
      $ awk -F ':' '{print $1}' /etc/passwd
      > root
      > daemon
      > bin
      > sys



      //显示/etc/passwd文件中的账户和账户对应的shell信息,而账户与shell之间以tab键分割
      $ awk -F ':' '{print $1"\t"$7}' /etc/passwd
      > root /bin/bash
      > daemon /bin/sh
      > bin /bin/sh
      > sys /bin/sh

      //格式化输出
      $ awk '{printf "%-8s %-10s\n",$1,$7}' /etc/passwd
      > root /bin/bash
      > daemon /bin/sh
      > bin /bin/sh
      > sys /bin/sh

      //显示/etc/passwd的账户和账户对应的shell,而账户与shell之间以逗号分割
      //而且在所有行添加列名name,shell,在最后一行添加"blue,/bin/nosh"。
      // 遇到BEGIN和END关键字,则二者分别在 读取文件之前 和 所有记录读完之后 执行
      $ awk -F ':' 'BEGIN {print "name,shell"} {print $1","$7} END {print "blue,/bin/nosh"}' /etc/passwd
      > name,shell
      > root,/bin/bash
      > daemon,/bin/sh
      > bin,/bin/sh
      > sys,/bin/sh
      > blue,/bin/nosh
    • pattern使用示例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      // 搜索/etc/passwd有root关键字的所有行,
      $ awk -F: '/root/' /etc/passwd
      > root:x:0:0:root:/root:/bin/bash
      // 匹配了pattern(这里是root)的行才会执行action(本例未指定action,默认输出每行的内容,即$0,下例指定action)。
      // 搜索支持正则,例如找root开头的: awk -F: '/^root/' /etc/passwd

      //指定action{print $7}。搜索/etc/passwd有root关键字的所有行,并显示对应的shell
      $ awk -F: '/root/{print $7}' /etc/passwd
      > /bin/bash

      //过滤第一列大于2的行(action就不指定了)
      $ awk '$1>2' /etc/passwd

      //过滤第一列等于2的行(action就不指定了)
      $ awk '$1=2' /etc/passwd

      //过滤第一列大于2并且第二列等于'Are'的行(action就不指定了)
      $ awk '$1>2 && $2=="Are"' /etc/passwd

      //过滤第一列包含 "th"的行(action就不指定了)
      $ awk '$1 ~ /th/' /etc/passwd

      //过滤第一列包含 "th"的行,忽略大小写(action就不指定了)
      $ awk 'BEGIN{IGNORECASE=1} $1 ~ /th/' /etc/passwd

      //过滤第一列不包含 "th"的行,忽略大小写(action就不指定了)
      $ awk 'BEGIN{IGNORECASE=1} $1 !~ /th/' /etc/passwd

      //过滤整行不包含 "th"的行,忽略大小写(action就不指定了)
      $ awk 'BEGIN{IGNORECASE=1} !/th/' /etc/passwd
    • 变量赋值和使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      // 内建变量赋值。使用BEGIN替代-F,其中FS是awk内建变量
      $ awk 'BEGIN{FS=":"} {print $1}' /etc/passwd
      > root
      > daemon
      > bin
      > sys

      // 内建变量使用。使用BEGIN替代-F,其中FS是awk内建变量,并且打印文件名,使用FILENAME内建变量
      $ awk 'BEGIN{FS=":"} {print FILENAME,$1}' /etc/passwd
      > /etc/passwd root
      > /etc/passwd daemon
      > /etc/passwd bin
      > /etc/passwd sys

      //-v变量赋值用法
      $ awk -F: -va=1 -vb=s '/root/{print $7,$7+a,$7b}' /etc/passwd
      > /bin/bash 1 /bin/bashs
      > /bin/false 1 /bin/falses
      //BEGIN同理
      $ awk -F: 'BEGIN {a=1;b=s} /root/{print $7,$7+a,$7b}' /etc/passwd
      > /bin/bash 1 /bin/bashs
      > /bin/false 1 /bin/falses
    • 其他一些实例

      1
      2
      3
      4
      5
      //计算文件大小
      $ ls -l *.txt | awk '{sum+=$6} END {print sum}'

      //从文件中找出长度大于80的行
      $ awk 'length>80' /etc/passwd

      awk还支持条件语句和循环,使用较少,本文不再展开。


B

C

cat-连接文件或标准输入至标准输出

  • 简介:

  • cat命令是linux下的一个文本输出命令,其功能是连接文件或标准输入至标准输出,常用于显示文件内容 。cat是concatenate(串联)的缩写。

  • 语法:

    • cat [选项参数] [--help] [--version] fileName
  • 选项值:

    • -n 或 –number

      • 由 1 开始对所有输出的行数编号。
    • -b 或 –number-nonblank

      • 和 -n 相似,只不过对于空白行不编号。
    • -s 或 –squeeze-blank

      • 当遇到有连续两行以上的空白行,就代换为一行的空白行。
    • -v 或 –show-nonprinting

      • 以^和M-显示不可打印字符,除LFD与TAB
    • -E 或 –show-ends

      • 在每行结束处显示 $。
    • -T 或 –show-tabs

      • 将 TAB 字符显示为 ^I。这有助于排查缩进错误。
    • -A, –show-all

      • 等价于-vET显示所有,以$结尾。
    • -e

      • 在每行行尾添加$,用以标记
    • -t

      • 等价于”-vT”选项;
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    //打印多个文件的内容
    $ cat one.txt two.txt
    > This line is from one.txt
    > This line is from two.txt

    //将stdin和另一个文件中的数据组合在一起,其中-被作为stdin文本的文件名。
    $ echo 'Text through stdin' | cat - file.txt

    //把 textfile1 的文档内容加上行号后输入 textfile2 这个文档里:
    $ cat -n textfile1 > textfile2

    //把 textfile1 和 textfile2 的文档内容加上行号(空白行不加)之后将内容附加到 textfile3 文档里:
    $ cat -b textfile1 textfile2 >> textfile3

    //有时候,我们只希望抓取文件中感兴趣的关键字
    $ cat fileName | grep keyword

    //清空某个文件内容
    $ cat /dev/null > /test.txt

    //创建文件,并把标准输入输出到filename文件中
    $ cat>filename,

    //以EOF作为输入结束,将输入流重定向覆盖filename文件,可用于创建文件,追加使用>>
    $ cat>filename<<EOF
  • 注意事项

    现在我们知道cat << EOF语句表示 打印以EOF输入字符为标准输入结束的流内容,EOF是“end of file”, 表示文本结束符。用在这里起到什么作用?首先必须要说明的是EOF在这里没有特殊的含义,你可以使用FOE或 OOO等(当然也不限制在三个字符或大写字符)可以把EOF替换成其他东西。cat << OOF的含义即为打印以OOF输入字符为标准输入结束的流内容,代码如下图

    cat>filename,创建文件,并把标准输入输出到filename文件中,以ctrl+d作为输入结束。

    cat>filename<<EOF等价于cat<filename,执行顺序固定是输入早于输出


cd——切换当前工作目录

  • 简介:

    • Linux cd命令用于切换当前工作目录至 dirName(目录参数)。
    • 其中 dirName 表示法可为绝对路径或相对路径。若目录名称省略,则变换至使用者的 home 目录 (也就是刚 login 时所在的目录)。
    • 另外,”~” 也表示为 home 目录 的意思,”.” 则是表示目前所在的目录,”..” 则表示目前目录位置的上一层目录;”-“表示上次所在目录
  • 语法:

    • cd [dirName]
    • dirName:要切换的目标目录。
  • 选项值:

  • 代码示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //跳到 /usr/bin/
    $ cd /usr/bin

    //跳到自己的 home 目录
    $ cd ~

    //跳到目前目录的上层
    $ cd ..

    //跳到目前目录的上上两层
    $ cd ../..

    //返回之前的目录
    $ cd -

chmod-设置文件权限

  • 简介:

  • Linux/Unix 的文件调用权限分为三级:用户,用户组,其他用户。利用 chmod 可以藉以控制文件如何被他人所调用。

  • 语法:

    • chmod [选项参数] mode filename
    • 其中mode为权限设定字串,格式如下[ugoa...][[+-=][rwxX]...][,...]
      • u 表示该文件的拥有者,g 表示与该文件的拥有者属于同一个用户组(group)者,o 表示其他用户,a 表示这三者皆是。
      • +表示增加权限、- 表示取消权限、= 表示唯一设定权限。
      • r 表示可读取,w 表示可写入,x 表示可执行,X 表示只有当该文件是个子目录或者该文件已经被设定过为可执行。
      • x 权限的位置上还可能出现t表示文件有执行权限并设置了粘滞位权限、T表示文件没有执行权限并设置了粘滞位权限、S表示setuid(S)的特殊权限,setuid权限允许可执行文件以其拥有者的权限来执行,即使这个可执行文件是由其他用户运行的。
      • 同时,mode也可以使用3位八进制数来表示,每一位按顺序分别对应用户、用户组和其他用户。其中r=4,w=2,x=1。所以:
        • rwx属性:4+2+1=7;
        • rw-属性:4+2=6;
        • r-x属性:4+1=5;
        • 以此类推…
  • 选项值:

    • -c

      • 若该文件权限确实已经更改,才显示其更改动作
    • -f

      • 若该文件权限无法被更改也不要显示错误讯息
    • -v

      • 显示权限变更的详细资料
    • -R

      • 对目前目录下的所有文件与子目录进行相同的权限变更(即以递回的方式逐个变更)
    • –help

      • 显示辅助说明
    • –version

      • 显示版本
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    //将文件 file1.txt 设为所有人皆可读取
    $ chmod ugo+r file1.txt
    或
    $ chmod a+r file1.txt

    //将 file1.txt 设定为只有该文件拥有者可以执行 :
    $ chmod u+x file1.txt

    //将文件 file1.txt 与 file2.txt 设为该文件拥有者,与其所属同一用户组的用户可写入,但其他以外的人则不可写入
    $ chmod ug+w,o-w file1.txt file2.txt

    //将目前目录下的所有文件与子目录皆设为任何人可读取 :
    $ chmod -R a=rwx *
    或
    $ chmod 777 . -R
    或
    $ chmod 777 "$(pwd)" -R

    //给所有用户赋值读写执行权限
    $ chmod a=rwx file
    或
    $ chmod 777 file

    //给文件拥有用户和其相同用户组用户读写执行权限,其他用户可执行权限。
    $ chmod ug=rwx,o=x file
    或
    $ chmod 771 file

    //给特定目录加粘滞位权限(设置粘滞位后,只有目录的所有者才能够删除目录中的文件,即使其他人有该目录的写权限也无法执行删除操作。)
    $ chmod a+t directoryName
  • 注意事项

    chmod 4755 filename 使此程序具有root的权限


chown-更改文件或目录的所有权

  • 简介:
    • Linux/Unix 是多人多工操作系统,所有的文件皆有拥有者。利用 chown 将指定文件的拥有者改为指定的用户或组,用户可以是用户名或者用户ID;组可以是组名或者组ID;文件是以空格分开的要改变权限的文件列表,支持通配符。
- 一般来说,这个指令只有是由系统管理者(root)所使用,一般使用者没有权限可以改变别人的文件拥有者,也没有权限可以自己的文件拥有者改设为别人。只有系统管理者(root)才有这样的权限。
  • 语法:

    • chown [选项参数] [--help] [--version] user[:group] file...
    • 其中user表示新的文件拥有者的使用者 ID。group表示新的文件拥有者的使用者组(group)
  • 选项值:

    • -c

      • 显示更改的部分的信息
    • -f

      • 忽略错误信息
    • -h

      • 修复符号链接
    • -v

      • 显示详细的处理信息
    • -R

      • 处理指定目录以及其子目录下的所有文件
    • –help

      • 显示辅助说明
    • –version

      • 显示版本
  • 代码示例

    1
    2
    3
    4
    5
    //将文件 file1.txt 的拥有者设为 runoob,群体的使用者 runoobgroup :
    $ chown runoob:runoobgroup file1.txt

    //将目前目录下的所有文件与子目录的拥有者皆设为 runoob,群体的使用者 runoobgroup:
    $ chown -R runoob:runoobgroup *
  • 注意事项


cp-于复制文件或目录

  • 简介:

  • Linux cp命令主要用于复制文件或目录。

  • 语法:

    • cp [options] source dest 或 cp [options] source... directory
  • 选项值:

    • -a

      • 此选项通常在复制目录时使用,它保留链接、文件属性,并复制目录下的所有内容。其作用等于dpR参数组合。
    • -d

      • 复制时保留链接。这里所说的链接相当于Windows系统中的快捷方式。
    • -f

      • 覆盖已经存在的目标文件而不给出提示。
    • -i

      • 与-f选项相反,在覆盖目标文件之前给出提示,要求用户确认是否覆盖,回答”y”时目标文件将被覆盖。
    • -p

      • 除复制文件的内容外,还把修改时间和访问权限也复制到新文件中。
    • -r

      • 若给出的源文件是一个目录文件,此时将复制该目录下所有的子目录和文件。
    • -l

      • 不复制文件,只是生成链接文件。
  • 代码示例

    1
    2
    //使用指令"cp"将当前目录"test/"下的所有文件复制到新目录"newtest"下
    $ cp –r test/ newtest
  • 注意事项

    用户使用该指令复制目录时,必须使用参数”-r”或者”-R”。


curl——发送各种HTTP请求

  • 简介:

    • curl命令的用途广泛,其功能包括下载、发送各种HTTP请求以及指定HTTP头部。
    • cURL默认会将下载文件输出到stdout,将进度信息输出到stderr。如果不想显示进度信息,可以使用–silent选项
    • cURL可以使用HTTP、HTTPS、FTP协议在客户端与服务器之间传递数据。它支持POST、cookie、认证、从指定偏移处下载部分文件、参照页(referer)、用户代理字符串、扩展头部、限速、文件大小限制、进度条等特性。cURL可用于网站维护、数据检索以及服务器配置核对。
  • 语法:

    • curl [选项] url
  • 选项值:

    • –silent

      • 不显示进度信息
    • -O

      • 指明将下载数据写入文件,采用从URL中解析出的文件名。注意,其中的URL必须是完整的,不能仅是站点的域名
      • $ curl www.knopper.net/index.htm -O
    • -o

      • 指明将下载数据写入文件,可以指定输出文件名。如果使用了该选项,只需要写明站点的域名就可以下载其主页了
      • $ curl www.knopper.net -o knoppix_index.html
    • –progress

      • 在下载过程中显示形如#的进度条
      • $ curl http://knopper.net -o index.html --progress
        ################################## 100.0%
    • -C

      • 够从特定的文件偏移处继续下载,偏移量是以字节为单位的整数。
      • $ curl URL/file -C offset
      • 如果只是想断点续传,那么cURL不需要指定准确的字节偏移。要是你希望cURL推断出正确的续传位置,请使用选项-C -。cURL会自动计算出应该从哪里开始续传
      • $ curl -C - www.xxx.com
    • –referer

      • 指定参照页字符串。参照页(referer)①是位于HTTP头部中的一个字符串,用来标识用户是从哪个页面到达当前页面的。如果用户通过点击页面A中的某个链接跳转到了页面B,那么页面B头部的参照页字符串就会包含页面A的URL。
      • 一些动态页面会在返回HTML数据前检测参照页字符串。例如,如果用户是通过Google搜索来到了当前页面,那么页面上就可以显示一个Google的logo;
      • $ curl --referer Referer_URL target_URL 如 $ curl --referer http://google.com http://knopper.org
    • –cookie

      • 指定储HTTP操作过程中使用到的cookie,cookies需要以name=value的形式来给出。多个cookie之间使用分号分隔
      • $ curl http://example.com --cookie "user=username;pass=hack"
    • –cookie-jar

      • 可以将cookie另存为文件
      • $ curl URL --cookie-jar cookie_file
    • -b

      • 指定以一个存储了Cookie值的本地文件内容作为cookie
      • $ curl -b stored_cookies_in_file www.baidu.com
    • –user-agent或-A

      • 设置用户代理字符串,如果不指定用户代理(user agent),一些需要检验用户代理的页面就无法显示。例如,有些旧网站只能在Internet Explorer(IE)下正常工作。如果使用其他浏览器,则会提示只能用IE访问。这是因为这些网站检查了用户代理。
      • $ curl URL --user-agent "Mozilla/5.0"
    • -H

      • 发送HTTP头部信息
      • $ curl -H "Host: www.knopper.net" -H "Accept-language: en" URL
    • –limit-rate

      • 如果多个用户共享带宽有限,我们可以用–limit-rate限制cURL的下载速度。在命令中用k(千字节)和m(兆字节)指定下载速度限制
      • $ curl URL --limit-rate 20k
    • –max-filesize

      • 指定可下载的最大文件大小。如果文件大小超出限制,命令返回一个非0的退出码。如果文件下载成功,则返回0。
      • $ curl URL --max-filesize bytes
    • -u

      • 指定用户名和密码完成HTTP或FTP认证
      • $ curl -u user:pass http://test_auth.com
      • 如果你喜欢经提示后输入密码,只需要使用用户名即可
      • $ curl -u user http://test_auth.com
    • -I 或–head

      • 只打印HTTP头部信息,无须下载远程文件。只检查头部信息就足以完成很多检查或统计。例如,如果要检查某个页面是否能够打开,并不需要下载整个页面内容。只读取HTTP响应头部就足够了。
      • $ curl -I http://knopper.net
    • -E或–cert

      • 指定本地证书
      • $ curl -E mycert.pem https:/itbilu.com
    • -T 或–upload-file

      • 上传文件
      • $ curl -T ./index.html www.uploadhttp.com/receive.cgi
    • -d 或–data

      • POST提交表单数据
      • $ curl -X POST --data 'keyword=linux' itbilu.com
    • -X

      • 拓展http请求方法,使curl支持put,delete等
      • $ curl -X DELETE itbilu.com/examlple.html
      • $ curl -X PUT --data 'keyword=linux' itbilu.com
  • 代码示例

    1
    2
    3
    4
    5
    //访问一个网页时,可以使用curl命令后加上要访问的网址
    $ curl www.baidu.com
    > <!DOCTYPE html>
    > <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;...
    //我们就看到所访问网址的页面源码。
  • 注意事项


cut——按列切分文件

  • 简介:

    • cut命令能够提取指定位置或列之间的字符。你可以指定每列的分隔符。在cut的术语中,每列被称为一个字段。
    • cut命令可以按列,而不是按行来切分文件。该命令可用于处理使用固定宽度字段的文件、CSV文件或是由空格分隔的文件(例如标准日志文件)。
    • cut 命令从文件的每一行剪切字节、字符和字段并将这些字节、字符和字段写至标准输出。如果不指定 File 参数,cut 命令将读取标准输入。必须指定 -b、-c 或 -f 标志之一。
  • 语法:

    • cut [选项参数] [file]
  • 选项值:

    • -b

      • 以字节为单位进行分割。这些字节位置将忽略多字节字符边界,除非也指定了 -n 标志。
      • range可以为如下格式,如cut -b 2-5 file
        • N-:从第N个字节、字符、字段到结尾;
        • N-M:从第N个字节、字符、字段到第M个(包括M在内)字节、字符、字段;
        • -M:从第1个字节、字符、字段到第M个(包括M在内)字节、字符、字段。
    • -c

      • 以字符为单位进行分割。
      • range可以为如下格式,如cut -c 2-5 file
        • N-:从第N个字节、字符、字段到结尾;
        • N-M:从第N个字节、字符、字段到第M个(包括M在内)字节、字符、字段;
        • -M:从第1个字节、字符、字段到第M个(包括M在内)字节、字符、字段。
    • -d

      • 自定义分隔符,默认为制表符。
    • -f

      • 指定要提取的列。FIELD_LIST是需要显示的列。它由列号组成,彼此之间用逗号分隔。
    • -n

      • 取消分割多字节字符。仅和 -b 标志一起使用。如果字符的最后一个字节落在由 -b 标志的 List 参数指示的范围之内,该字符将被写出;否则,该字符将被排除
    • –complement

      • 显示出没有被-f指定的那些字段。
    • –output-delimiter

      • 指定输出分隔符。在显示多组数据时,该选项尤为有用
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    //从使用制表符作为分隔符的文件中提取列
    $ cat student_data.txt
    > No Name Mark Percent
    > 1 Sarath 45 90
    > 2 Alex 49 98
    > 3 Anu 45 90
    $ cut -f 1 student_data.txt
    > No
    > 1
    > 2
    > 3

    //要想提取多个字段,就得给出由逗号分隔的多个字段编号
    $ cut -f 2,4 student_data.txt
    > Name Percent
    > Sarath 90
    > Alex 98
    > Anu 90

    //使用cut处理由分号分隔的字段
    $ cat delimited_data.txt
    > No;Name;Mark;Percent
    > 1;Sarath;45;90
    > 2;Alex;49;98
    > 3;Anu;45;90
    $ cut -f2 -d";" delimited_data.txt
    > Name
    > Sarath
    > Alex
    > Anu

    //打印第2个到第5个字符
    $ cat range_fields.txt
    > abcdefghijklmnopqrstuvwxyz
    > abcdefghijklmnopqrstuvwxyz
    > abcdefghijklmnopqrstuvwxyz
    //打印第2个到第5个字符
    $ cut -c2-5 range_fields.txt
    > ab
    > ab
    > ab
    //--output-delimiter指定输出分隔符
    $ cut range_fields.txt -c1-3,6-9 --output-delimiter ","
    > abc,fghi
    > abc,fghi
    > abc,fghi

D

date-显示或设定系统的日期与时间

  • 简介:

  • Linux date命令可以用来显示或设定系统的日期与时间,在显示方面,使用者可以设定欲显示的格式,格式设定为一个加号后接数个标记。

  • 语法:

    • date [-u] [-d datestr] [-s datestr] [--utc] [--universal] [--date=datestr] [--set=datestr] [--help] [--version] [+FORMAT] [MMDDhhmm[[CC]YY][.ss]]
  • 选项值:

    • -d datestr

      • 显示 datestr 中所设定的时间 (非系统时间)
    • –help

      • 显示辅助讯息
    • -s datestr 或 –set

      • 将系统时间设为 datestr 中所设定的时间
    • -u 或 –utc

      • 显示目前的格林威治时间
    • –version

      • 显示版本编号
    • –date

      • 显示该选项指定的输入日期所设定的时间
  • 注意事项

    格式字段datestr必须以+号开头

    当您不希望出现无意义的 0 时(比如说 1999/03/07),则可以在标记中插入 - 符号,比如说 date ‘+%-H:%-M:%-S’ 会把时分秒中无意义的 0 给去掉,像是原本的 08:09:04 会变为 8:9:4。

    date命令的最小精度是秒

    只有取得权限者(比如说 root)才能设定系统时间。当您以 root 身分更改了系统时间之后,请记得以 clock -w 来将系统时间写入 CMOS 中,这样下次重新开机时系统时间才会持续抱持最新的正确值。

    datestr的值有如下这些可选:

    • %

      • 印出 %
    • %n

      • 下一行
    • %t

      • 跳格,一个tab
    • %H

      • 小时(00-23)
    • %I

      • 小时(01-12)
    • %k

      • 小时(0-23)
    • %l

      • 小时(1-12)
    • %M

      • 分钟(00-59)
    • %p

      • 显示本地 AM 或 PM
    • %S

      • 秒(00-61)
    • %T

      • 直接显示时间 (24 小时制)
    • %Z

      • 显示时区
    • %a

      • 星期几 (Sun-Sat)
    • %A

      • 星期几 (Sunday-Saturday)
    • %b

      • 月份 (Jan-Dec)
    • %B

      • 月份 (January-December)
    • %c

      • 直接显示日期与时间
    • %d

      • 日 (01-31)
    • %D

      • 直接显示日期 (mm/dd/yy)
    • %h

      • 同 %b
    • %j

      • 一年中的第几天 (001-366)
    • %m

      • 月份 (01-12)
    • %U

      • 一年中的第几周 (00-53) (以 Sunday 为一周的第一天的情形)
    • %w

      • 一周中的第几天 (0-6)
    • %W

      • 一年中的第几周 (00-53) (以 Monday 为一周的第一天的情形)
    • %x

      • 直接显示日期 (mm/dd/yy)
    • %y

      • 年份的最后两位数字 (00-99)
    • %Y

      • 完整年份 (0000-9999)
  • 代码示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    //读取当前时间
    $ date
    > Thu May 20 23:09:04 IST 2010

    //打印unix时间
    $ date +%s
    > 1290047248

    //将输入的时间转换成unix时间
    $ date --date "Wed mar 15 08:09:16 EDT 2017" +%s
    > 1489579718

    //读取当前时间
    $ date "+%d %B %Y"
    > 20 May 2010

    //设置日期和时间
    $ date -s "21 June 2009 11:01:22"

    //显示时间后换行,再显示目前日期
    $ date '+%T%n%D'
    >23:09:00
    >07/09/19

    //按自己的格式输出
    $ date '+usr_time: $1:%M %P -hey'
    > usr_time: $1:16 下午 -hey

E

echo-打印内容

  • 简介:

  • echo命令是linux中最基础的命令,也是很常用的命令,功能说明用以显示文字。

  • 语法:

    • echo [-ne][字符串]或 echo [--help][--version]
  • 选项值:

    • -n

      • echo会在输出文本的尾部追加一个换行符。可以使用选项-n来禁止这种行为。
    • -e

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      激活转义字符。使用-e选项时,若字符串中出现以下字符,则特别加以处理,而不会将它当成一般文字输出:
      \a 发出警告声;
      \b 删除前一个字符;
      \c 最后不加上换行符号;
      \f 换行但光标仍旧停留在原来的位置;
      \n 换行且光标移至行首;
      \r 光标移至行首,但不换行;
      \t 插入tab;
      \v 与\f相同;
      \\ 插入\字符;
      \e 打印彩色文本
      文本颜色是由对应的色彩码来描述的。其中包括:重置=0,黑色=30,红色=31,绿色=32,黄色=33,蓝色=34,洋红=35,青色=36,白色=37。
      对于彩色背景,经常使用的颜色码是:重置=0,黑色=40,红色=41,绿色=42,黄色=43,蓝色=44,洋红=45,青色=46,白色=47。
      用法:echo -e "\e[1;31m This is red text \e[0m"
      其中\e[1;31m是一个转义字符串,可以将颜色设为红色,\e[0m将颜色重新置回。
      \nnn 插入nnn(八进制)所代表的ASCII字符;
  • 注意事项

    echo的单引号不会解释文本中的特殊字符,双引号会。

  • 代码示例

    1
    2
    3
    4
    5
    $ a=1;
    - echo '$a+1'
    > a+1
    $ echo "$a+1"
    > 1+1

F

find-查找文件

  • 简介:

    • find命令的工作方式如下:沿着文件层次结构向下遍历,匹配符合条件的文件,执行相应
      的操作。默认的操作是打印出文件和目录,这也可以使用-print选项来指定。
  • 语法:

    • find path -option [ -print ] [ -exec -ok command ] {} \;
  • 选项值:

    • -mount或 -xdev

      • 只检查和指定目录在同一个文件系统下的文件,避免列出其它文件系统中的文件
    • -newer file

      • 找出比file文件更新的(更近的修改时间)所有文件。
    • -anewer file

      • 查找比文件 file 更晚被读取过的文件
    • -cnewer file

      • 查找比文件 file 更新的文件
    • -amin n

      • 根据用户最近一次访问文件的时间查找,查找在过去 n 分钟内被读取过的文件,数字n前面可以加上-或+。-表示小于,+表示大于。
    • -atime n

      • 根据用户最近一次访问文件的时间查找,查找在过去 n 天内被读取过的文件,数字n前面可以加上-或+。-表示小于,+表示大于。
    • -cmin n

      • 根据文件元数据(例如权限或所有权)最后一次改变的时间查找,查找在过去 n 分钟内被修改过,数字n前面可以加上-或+。-表示小于,+表示大于。
    • -ctime n

      • 根据文件元数据(例如权限或所有权)最后一次改变的时间查找,查找在过去 n 天内被修改过,数字n前面可以加上-或+。-表示小于,+表示大于。
    • -mtime n

      • 根据文件内容最后一次被修改的时间查找,查找在过去 n 分钟内被修改过,数字n前面可以加上-或+。-表示小于,+表示大于。
    • -mmin n

      • 根据文件内容最后一次被修改的时间查找,查找在过去 n 天内被修改过,数字n前面可以加上-或+。-表示小于,+表示大于。
    • -ipath p, -path p

      • 查找路径名称符合 p 的文件,ipath 会忽略大小写。p一般可以为通配符如’/slynux/‘
    • -regex r,-iregex r

      • 查找路径名称符合 r 的文件。r 一般可以为正则表达式
    • -name name, -iname name

      • 查找文件名称符合 name 的文件。iname 会忽略大小写。这个模式可以是通配符,也可以是正则表达式。如’*.txt’能够匹配所有名字以.txt结尾的文件或目录
    • -size n

      • 可以根据文件的大小展开搜索,n可以为+2k,数字前面可以加上-或+。-表示小于,+表示大于。单位取值可以为
        • b:块(512字节)。
        • c:字节。
        • w:字(2字节)。
        • k:千字节(1024字节)。
        • M:兆字节(1024K字节)。
        • G:吉字节(1024M字节)。
    • -maxdepth n,–mindepth n

      • -maxdepth和–mindepth选项可以限制find命令遍历的目录深度。这可以避免find命令没完没了地查找。
    • –user USER

      • 找出由某个特定用户权限下所拥有的文件。参数USER可以是用户名或UID。
    • -prune n

      • 在搜索时排除某些文件或目录,一般与其他选项一起使用,如.git目录应该被排除在外:
        $ find devel/source_path -name '.git' -prune -o -type f -print
    • -perm n

      • 指明find应该只匹配具有特定权限值的文件。n为文件权限
        $ find devel/source_path -name '.git' -prune -o -type f -print
    • pid n

      • process id 是 n 的文件
    • -type t

      • 类Unix系统将一切都视为文件。文件具有不同的类型,例如普通文件、目录、字符设备、块
        设备、符号链接、硬链接、套接字以及FIFO等。可以使用-type选项对文件搜索进行过滤。借助这个选项,我们可以告诉find命令只匹配指定类型的文件。t的取值可以是

        • d:目录

        • c: 字符设备

        • b: 块设备

        • p: FIFO具名贮列

        • f: 一般文件

        • l: 符号链接

        • s: socket套接字

  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    //列出给定目录下所有的文件和子目录 find base_path
    $ find . -print
    > .history
    > Downloads
    > Downloads/tcl.fossil

    //够匹配所有名字以.txt结尾的文件或目录。
    $ find /home/slynux -name '*.txt' -print

    //find命令支持逻辑操作符。-a和-and选项可以执行逻辑与(AND)操作,-o和-or选项可以执行逻辑或(OR)操作。
    $ find . \( -name '*.txt' -o -name '*.pdf' \) -print
    > ./text.pdf
    > ./new.txt


    //使用-and操作符选择名字以s开头且其中包含e的文件
    $ find . \( -name '*e*' -and -name 's*' \)
    > ./some.jpg

    //匹配.py或.sh文件
    $ find . -regex '.*\.(py\|sh\)$'

    //find也可以用!排除匹配到的模式,匹配所有不以.txt结尾的文件
    $ find . ! -name "*.txt" -print
    >

    //打印出在最近7天内被访问过的所有文件
    $ find . -type f -atime -7 -print

    //打印出访问时间超过7天的所有文件。
    $ find . -type f -atime +7 -print

    //找出比file.txt修改时间更近的所有文件
    $ find . -type f -newer file.txt -print

    //打印出用户slynux拥有的所有文件
    $ find . -type f -user slynux -print

    //利用find执行相应操作
    //删除匹配的文件
    //find命令的-delete选项可以删除所匹配到的文件。下面的命令能够从当前目录中删除.swp文件:
    $ find . -type f -name "*.swp" -delete

    /利用find执行相应操作
    //执行命令,利用-exec选项,find命令可以结合其他命令使用。
    //将某位用户(比如root)所拥有的全部文件的所有权更改成另一位用户(比如用户www-data)
    //那么可以用-user找出root拥有的所有文件,然后用-exec更改所有权。
    //在下面的例子中,对于每一个匹配的文件,find命令会将{}替换成相应的文件名并更改该文件的所有权。
    $ find . -type f -user root -exec chown slynux {} \;
    //find命令使用一对花括号{}代表文件名。
  • 注意事项

    注意 find . -type f -user root -exec chown slynux {} ; 该命令结尾的;。必须对分号进行转义,否则shell会将其视为find命令的结束,而非chown命令的结束。

    为每个匹配到的文件调用命令可是个不小的开销。如果指定的命令接受多个参数(如chown),你可以换用加号(+)作为命令的结尾。这样find会生成一份包含所有搜索结果的列表,然后将其作为指定命令的参数,一次性执行。

    我们无法在-exec选项中直接使用多个命令。该选项只能够接受单个命令,不过我们可以耍一个小花招。把多个命令写到一个 shell脚本中(例如command.sh),然后在-exec中使用这个脚本:
    -exec ./commands.sh {} ;


ftp/lftp——使用ftp协议共享文件

  • 简介:

    • 文件传输协议(File Transfer Protocol,FTP)是一个古老的协议,在很多公共站点上用于文件共享。
    • FTP服务器通常运行在端口21上。远程主机上必须安装并运行FTP服务器才能使用FTP。
    • 我们可以使用传统的ftp命令或更新的lftp命令访问FTP服务器。两者都支持下面要讲到的命令。很多公共网站都是用FTP共享文件
  • 使用步骤

    1. 要连接FTP服务器传输文件,可以使用
      • $ lftp username@ftphost
      • 它会提示你输入密码,然后显示一个像下面这样的登录提示符
        lftp username@ftphost:~>
    2. 你可以在提示符后输入各种命令,如下所示
      • cd directory:更改远程主机目录。
      • lcd:更改本地主机目录。
      • mkdir:在远程主机上创建目录。
      • ls:列出远程主机当前目录下的文件。
      • get FILENAME:将文件下载到本地主机的当前目录中。
        • lftp username@ftphost:~> get filename
      • put filename:将文件从当前目录上传到远程主机。
        • lftp username@ftphost:~> put filename
      • quit命令可以退出lftp会话
  • 注意事项

    lftp提示符支持命令自动补全。


G

grep——在文件中搜索文本

  • 简介:

    • Linux grep命令用于查找文件里符合条件的字符串。

    • grep指令用于查找内容包含指定的范本样式的文件,如果发现某文件的内容符合所指定的范本样式,预设grep指令会把含有范本样式的那一行显示出来。

    • 若不指定任何文件名称,或是所给予的文件名为”-“,则grep指令会从标准输入设备读取数据。

  • 语法:

    • grep [选项参数][范本样式][文件或目录...]
  • 选项值:

    • -a 或 –text

      • 不要忽略二进制的数据。
    • -A<显示行数> 或 –after-context=<显示行数>

      • 除了显示符合范本样式的那一行之外,并显示该行之后的内容。
    • -b 或 –byte-offset

      • 可以打印出匹配出现在行中的偏移。字符在行中的偏移是从0开始计数,不是1。
    • -B<显示行数> 或 –before-context=<显示行数>

      • 除了显示符合样式的那一行之外,并显示该行之前的内容。
    • -c 或 –count

      • 计算符合样式的行数。需要注意的是-c只是统计匹配行的数量,并不是匹配的次数
    • -C<显示行数> 或 –context=<显示行数>或-<显示行数>

      • 除了显示符合样式的那一行之外,并显示该行之前和之后的内容。
    • -d <动作> 或 –directories=<动作>

      • 当指定要查找的是目录而非文件时,必须使用这项参数,否则grep指令将回报信息并停止动作。
    • -e<范本样式> 或 –regexp=<范本样式>

      • 指定字符串做为查找文件内容的样式。可以指定多个匹配模式。
    • -E 或 –extended-regexp

      • 将样式为延伸的普通表示法来使用。grep命令默认使用基础正则表达式。这是先前描述的正则表达式的一个子集。选项-E可以使grep使用扩展正则表达式。也可以使用默认启用扩展正则表达式的egrep命令。
    • -f<规则文件> 或 –file=<规则文件>

      • 指定规则文件,其内容含有一个或多个规则样式,让grep查找符合规则条件的文件内容,格式为每行一个规则样式。
    • -F 或 –fixed-regexp

      • 将样式视为固定字符串的列表。
    • -G 或 –basic-regexp

      • 将样式视为普通的表示法来使用。
    • -h 或 –no-filename

      • 在显示符合样式的那一行之前,不标示该行所属的文件名称。
    • -H 或 –with-filename

      • 在显示符合样式的那一行之前,表示该行所属的文件名称。
    • -i 或 –ignore-case

      • 忽略字符大小写的差别。
    • -l 或 –file-with-matches

      • 列出文件内容符合指定的样式的文件名称。
    • -L 或 –files-without-match

      • 列出文件内容不符合指定的样式的文件名称。
    • -n 或 –line-number

      • 在显示符合样式的那一行之前,标示出该行的列数编号。
    • -o 或 –only-matching

      • 只显示匹配PATTERN 部分。
    • -q 或 –quiet或–silent

      • 不显示任何信息。
    • -r 或 –recursive

      • 此参数的效果和指定”-d recurse”参数相同。
    • -s 或 –no-messages

      • 不显示错误信息。
    • -v 或 –revert-match

      • 显示不包含匹配文本的所有行。
    • -V 或 –version

      • 显示版本信息。
    • -w 或 –word-regexp

      • 只显示全字符合的列。
    • -x –line-regexp

      • 只显示全列符合的列。
    • -y

      • 此参数的效果和指定”-i”参数相同。
    • –color

      • 在输出行中着重标记出匹配到的模式,一般为–color=auto。
    • –include

      • 在搜索过程中使用通配符指定(include)某些文件
    • –exclude

      • 在搜索过程中使用通配符排除(exclude)某些文件
    • –exclude-dir

      • 在搜索过程中使用通配符以排除目录
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    //在当前目录中,查找有 file 字样的文件中包含 test 字符串的文件,并打印出该字符串的行。
    $ grep test *file*
    > file1:This a Linux testfile! #列出file1 文件中包含test字符的行
    > file_2:This is a linux testfile! #列出file_2 文件中包含test字符的行
    > file_2:Linux test #列出file_2 文件中包含test字符的行

    //以递归的方式查找符合条件的文件。
    //例如,查找指定目录/etc/acpi 及其子目录(如果存在子目录的话)下所有文件中包含字符串"update"的文件
    //并打印出该字符串所在行的内容
    $ grep -r update /etc/acpi
    > /etc/acpi/ac.d/85-anacron.sh:# (Things like the slocate updatedb cause a lot of IO.) Rather than
    > /etc/acpi/resume.d/85-anacron.sh:# (Things like the slocate updatedb cause a lot of IO.) Rather than
    > /etc/acpi/events/thinkpad-cmos: action=/usr/sbin/thinkpad-keys--update

    //反向查找。前面各个例子是查找并打印出符合条件的行,通过"-v"参数可以打印出不符合条件行的内容。
    //查找文件名中包含 file 的文件中不包含test 的行,
    $ grep -v test *file*

    //在stdin中搜索匹配特定模式的文本行
    $ $ echo -e "this is a word \n next line" | grep word
    > this is a word

    //使用扩展正则,只输出匹配到的文本,
    $ echo this is a line. | grep -o -E "[a-z]+\."
    > line

    //统计出匹配模式的文本行数,-c只是统计匹配行的数量,并不是匹配的次数
    $ grep -c "text" filename
    > 10

    //统计文件中匹配项的数量,可以使用下面的技巧
    $ echo -e "1 2 3 4\nhello\n5 6" | egrep -o "[0-9]" | wc -l
    > 6

    //打印多个文件包含Linux的行,输出行号
    $ grep linux -n sample1.txt sample2.txt
    > sample1.txt:2:linux is fun
    > sample2.txt:2:planetlinux

    //选项-b可以打印出匹配出现在行中的偏移。配合选项-o可以打印出匹配所在的字符或字节偏移
    $ echo gnu is not unix | grep -b -o "not"
    > 7:not

    //列出匹配模式所在的文件
    $ grep -l linux sample1.txt sample2.txt
    > sample1.txt
    > sample2.txt

    //对当前目录中对文本进行递归搜索
    $ grep "text" . -R -n

    //在匹配模式时不考虑字符的大小写
    $ echo hello world | grep -i "HELLO"
    > hello

    //使用-e指定多个匹配模式,打印出匹配任意一种模式的行,每个匹配对应一行输出。
    $ echo this is a line of text | grep -o -e "this" -e "line"
    > this
    > line

    //可以将多个模式定义在文件中。选项-f可以读取文件并使用其中的模式(一个模式一行)
    $ cat pat_file
    > hello
    > cool
    $ echo hello this is cool | grep -f pat_file
    > hello this is cool

    //使用--include选项在目录中递归搜索所有的 .c和 .cpp文件。
    //注意,some{string1,string2,string3}会被扩展成somestring1 somestring2 somestring3。
    $ grep "main()" . -r --include *.{c,cpp}
    >

    //使用选项--exclude在搜索过程中排除所有的README文件
    $ grep "main()" . -r --exclude "README"
    >

    //选项--exclude-dir可以排除目录:
    $ grep main . -r -exclude-dir CVS
    >

    //grep的静默输出。可以通过设置grep的静默选项(-q)来实现。
    //在静默模式中,grep命令不会输出任何内容。它仅是运行命令,然后根据命令执行成功与否返回退出状态。
    //0表示匹配成功,非0表示匹配失败。
    $ grep -q word file

H

head-打印文件的前n行

  • 简介:

  • head命令用于显示文件的开头的内容。在默认情况下,head命令显示文件的头10行内容。

  • 语法:

    • head [选项] fileName
  • 选项值:

    • -n<数字>

      • 指定显示头部内容的行数,负数时表示倒数n行
    • -c<字符数>

      • 指定显示头部内容的字符数;
    • -v

      • 总是显示文件名的头信息;
    • -q

      • 不显示文件名的头信息。
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    //显示 test.log 文件中前 20 行
    $ head -n 20 test.log
    //显示 test.log 文件中前 20 字节
    $ head -c 20 test.log

    //显示 test.log 文件中除了最后20行之外的所有行
    $ head -n -20 test.log


I

ifconfig——配置和展示网络信息

  • 简介:

  • ifconfig可设置网络设备的状态,或是显示目前的设置。注意和Windows的命令ipconfig区分。

  • 语法:

    • ifconfig [选项参数]
  • 选项值:

    • add<地址>

      • 设置网络设备IPv6的IP地址。
    • del<地址>

      • 删除网络设备IPv6的IP地址。
    • down

      • 关闭指定的网络设备。
    • <hw<网络设备类型><硬件地址>

      • 设置网络设备的类型与硬件地址。
    • io_addr<I/O地址>

      • 设置网络设备的I/O地址。
    • irq<IRQ地址>

      • 设置网络设备的IRQ。
    • media<网络媒介类型>

      • 设置网络设备的媒介类型。
    • mem_start<内存地址>

      • 设置网络设备在主内存所占用的起始地址。
    • metric<数目>

      • 指定在计算数据包的转送次数时,所要加上的数目。
    • mtu<字节>

      • 设置网络设备的MTU。
    • netmask<子网掩码>

      • 设置网络设备的子网掩码。
    • tunnel<地址>

      • 建立IPv4与IPv6之间的隧道通信地址。
    • up

      • 启动指定的网络设备。
    • -broadcast<地址>

      • 将要送往指定地址的数据包当成广播数据包来处理。
    • -pointopoint<地址>

      • 与指定地址的网络设备建立直接连线,此模式具有保密功能。
    • -promisc

      • 关闭或启动指定网络设备的promiscuous模式。
    • [IP地址]

      • 指定网络设备的IP地址。
    • [网络设备]

      • 指定网络设备的名称。
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //显示网络设备信息,ifconfig输出的最左边一列是网络接口名,右边的若干列显示对应的网络接口的详细信息。
    $ ifconfig
    > eth0 Link encap:Ethernet HWaddr 00:50:56:0A:0B:0C
    > .....以下省略

    //ifconfig会显示系统中所有活动网络接口的详细信息。不过,我们可以限制它只显示某个特定接口的信息
    $ ifconfig wlan0
    > wlan0 Link encap:EthernetHWaddr 00:1c:bf:87:25:d2

    //设置网络接口的IP地址
    $ ifconfig wlan0 192.168.0.80

    //设置此IP地址的子网掩码
    $ ifconfig wlan0 192.168.0.80 netmask 255.255.252.0
  • 注意事项


J

K

L

less——对文件或其它输出进行分页显示

  • 简介:

    • 对文件或其它输出进行分页显示。缓冲加载。
    • less 与 more 类似,但使用 less 可以随意浏览文件,而 more 仅能向前移动,却不能向后移动,而且 less 在查看之前不会加载整个文件。
  • 语法:

    • less [参数] 文件
  • 选项值:

    • -e

      • 当文件显示结束后,自动离开
    • -f

      • 强迫打开特殊文件,例如外围设备代号、目录和二进制文件
    • -g

      • 只标志最后搜索的关键词
    • -i

      • 忽略搜索时的大小写
    • -m

      • 显示类似more命令的百分比
    • -N

      • 显示每行的行号
    • -Q

      • 不使用警告音
    • -s

      • 显示连续空行为一行
    • -S

      • 行过长时间将超出部分舍弃
  • 快捷键

    • /<字符串>

      • 向下搜索”字符串”的功能
    • ?<字符串>

      • 向上搜索”字符串”的功能
    • n

      • 针对 / 或 ?的结果,重复前一个搜索
    • N

      • 针对 / 或 ?的结果,反向重复前一个搜索
    • b

      • 向后翻一页
    • d

      • 向后翻半页
    • h

      • 显示帮助界面
    • u

      • 向前滚动半页
    • y

      • 向前滚动一行
    • 空格键

      • 滚动一页
    • 回车键

      • 滚动一行
    • q / ZZ

      • 退出 less 命令
    • v

      • 使用配置的编辑器编辑当前文件
      • 对于提示的^X等操作,^X即为CTRL+X。
    • h

      • 显示 less 的帮助文档
    • F

      • 类似 tail -f 的效果,读取写入文件的最新内容, 按 ctrl+C 停止。
    • &

      • 开启模式匹配模式,可继续键入正则匹配,使得less仅显示匹配模式的行,而不是整个文件
    • m

      • mark的意思,开启mark模式,让你键入一个字符,比如我们键入了a,那么less会使用 a 标记文本的当前位置。当使用 less 查看大文件时,可以在任何一个位置作标记,可以通过命令导航到标有特定标记的文本位置
    • ‘

      • 单引号,开启goto mark模式,让你键入一个字符,到达你键入字符标记的位置,比如你键入啊,那么导航到你标记的 a 处
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //查看文件
    $ less log.log

    //ps查看进程信息并通过less分页显示
    $ ps -ef |less

    //查看命令历史使用记录并通过less分页显示
    $ history | less

    //浏览多个文件,输入 ":n" (n=next) 后,切换到 log2.log;输入":p" (p=preview)后,切换到log1.log
    $ less log1.log log2.log

ls———显示指定工作目录下之内容

  • 简介:

  • Linux ls命令用于显示指定工作目录下之内容(列出目前工作目录所含之文件及子目录)。它是list的简写。

  • 语法:

    • ls [选项参数] [name...]
  • 选项值:

    • -a

      • 显示所有文件及目录(ls内定将文件名或目录名称开头为”.”的视为隐藏档,不会列出)
    • -l

      • 除文件名称外,亦将文件型态、权限、拥有者、文件大小等资讯详细列出
    • -r

      • 将文件以相反次序显示(原定依英文字母次序)
    • -t

      • 将文件依建立时间之先后次序列出
    • -A

      • 同-a,但不列出”.”(目前目录)及”..”(父目录)
    • -R

      • 若目录下有文件,则以下之文件亦皆依序列出
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    //列出目前工作目录下所有名称是 s 开头的文件,越新的排越后面 
    $ ls -ltr s*

    //将 /bin 目录以下所有目录及文件详细资料列出
    $ ls -lR /bin

    //列出目前工作目录下所有文件及目录;目录于名称后加 "/", 可执行档于名称后加 "*"
    $ ls -AF

M

N

O

P

paste——合并文件的列

  • 简介:

  • paste指令会把每个文件以列对列的方式,一列列地加以合并。

  • 语法:

    • paste [选项参数][文件...]
  • 选项值:

    • -d<间隔字符>或–delimiters=<间隔字符>

      • 用指定的间隔字符取代跳格字符。
    • -s或–serial

      • 串列进行而非平行处理。
    • –help

      • 在线帮助。
    • –version

      • 显示帮助信息。
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    //合并file1.txt 和file2.txt,先看看这两个文件的内容
    $ cat file1.txt
    > 1
    > 2
    > 3
    > 4
    > 5
    $ cat file2.txt
    > slynux
    > gnu
    > bash
    > hack

    //使用paste合并
    $ paste file1.txt file2.txt
    > 1 slynux
    > 2 gnu
    > 3 bash
    > 4 hack
    > 5

    //默认的分隔符是制表符,也可以用-d指定分隔符
    $ paste file1.txt file2.txt -d ","
    > 1,slynux
    > 2,gnu
    > 3,bash
    > 4,hack
    > 5,

    //若使用paste指令的参数"-s",则可以将一个文件中的多行数据合并为一行进行显示。
    //例如,将文件"file"中的3行数据合并为一行数据进行显示,输入如下命令
    $ paste -s file
    > xiongdan 200 lihaihui 233 lymlrl 231
  • 注意事项

    参数”-s”只是将testfile文件的内容调整显示方式,并不会改变原文件的内容格式。


printf-打印内容

  • 简介:

    • printf命令接受引用文本或由空格分隔的参数。我们可以在printf中使用格式化字符串来指定字符串的宽度、左右对齐方式等。
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ printf "%-5s %-10s %-4s\n" No Name Mark ;
    - printf "%-5s %-10s %-4.2f\n" 1 Sarath 80.3456 11 ;
    - printf "%-5s %-10s %-4.2f\n" 2 James 90.9989 ;
    - printf "%-5s %-10s %-4.2f\n" 3 Jeff 77.564

    > No Name Mark
    > 1 Sarath 80.35
    > 2 James 91.00 14
    > 3 Jeff 77.56
  • 注意事项

    • %s、%c、%d和%f都是格式替换符(format substitution character),它们定义了该如何打印后续参数。%-5s指明了一个格式为左对齐且宽度为5的字符串替换(-表示左对齐)。如果不指明-,字符串就采用右对齐形式。

    • 宽度指定了保留给某个字符串的字符数量。对Name而言,其保留宽度是10。因此,任何Name字段的内容都会被显示在10字符宽的保留区域内,如果内容不足10个字符,余下的则以空格填充。

    • 对于浮点数,可以使用其他参数对小数部分进行舍入(round off)。 对于Mark字段,我们将其格式化为%-4.2f,其中.2指定保留两位小数。注意,在每行的格式字符串后都有一个换行符(\n)。


Q

R

read-从键盘或标准输入中读取文本

  • 简介:

    • 我们可以使用read以交互的形式读取用户输入,不过read能做的可远不止这些。编程语言的大多数输入库都是从键盘读取输入,当回车键按下的时候,标志着输入完毕。

    • 但有时候是没法按回车键的,输入结束与否是由读取到的字符数或某个特定字符来决定的。例如在交互式游戏中,当按下 + 键时,小球就会向上移动。那么若每次都要按下 + 键,然后再按回车键来确认已经按过 + 键,这就显然太低效了。

    • read命令提供了一种不需要按回车键就能够搞定这个任务的方法。

  • 语法:

    • read [-ers] [-a aname] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]
  • 选项值:

    • -a
  • 后跟一个变量,该变量会被认为是个数组,然后给其赋值,默认是以空格为分割符。

    • -d

      • 用特定的定界符delim_char作为输入行的结束,结果存入var变量(不包括边界符):read -d delim_char var
        1
        2
        3
        4
        5
        	$ read -d ":" var
        $ hello:

        $ echo $var
        > hello
    • -p

      • 后面跟提示信息,即在输入前打印提示信息。
        1
        2
        3
        4
        #!/bin/bash
        read -p "输入网站名:" website
        echo "你输入的网站名是 $website"
        exit 0
        1
        2
        $ 输入网站名:www.cherish.com
        > 你输入的网站名是 www.cherish.com
        1
        	
    • -e

      • 在输入的时候可以使用命令补全功能。
        1
        2
        3
        4
        5
        //以下实例输入字符 a 后按下 Tab 键就会输出相关的文件名(该目录存在的):
        $ read -e -p "输入文件名:" str
        $ 输入文件名:a
        a.out a.py a.pyc abc.txt
        > 输入文件名:a
        1
        	
    • -n

      • 后跟一个数字,定义输入文本的长度,很实用。
        例如:下面的语句从输入中读取n个字符并存入变量variable_name:
        read -n number_of_chars variable_name
        1
        2
        3
        4
        #!/bin/bash
        read -n 2 -p "请随便输入两个字符: " any
        echo "\n您输入的两个字符是:$any"
        exit 0
        1
        2
        $ 请随便输入两个字符: 12
        > 您输入的两个字符是:12
        1
        	
    • -s

    • 安静模式,在输入字符时不再屏幕上显示,例如login时输入密码。

    • -t

      • 后面跟秒数,定义输入字符的等待时间。

        1
        2
        3
        4
        5
        6
        7
        8
        #!/bin/bash
        if read -t 5 -p "输入网站名:" website
        then
        echo "你输入的网站名是 $website"
        else
        echo "\n抱歉,你输入超时了。"
        fi
        exit 0
        1
        2
        //执行程序不输入,等待 5 秒后:
        $ 输入网站名:

        抱歉,你输入超时了

        1
        	
    • -u

      • 后面跟fd,从文件描述符中读入,该文件描述符可以是exec新开启的。
  • 注意事项

  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    #!/bin/bash

    #这里默认会换行
    echo "输入网站名: "
    #读取从键盘的输入
    read website
    echo "你输入的网站名是 $website"
    exit 0 #退出
    1
    2
    3
    4
    5
    //常规用法
    $ ./testcat.sh
    > 输入网站名:
    $ www.baidu.com
    > 你输入的网站名是 www.baidu.com

rm-删除一个文件或者目录

  • 简介:

  • rm [options] filename…

  • 语法:

    • Linux rm命令用于删除一个文件或者目录。
  • 选项值:

    • -i

      • 删除前逐一询问确认。
    • -f

      • 即使原档案属性设为只读,亦直接删除,无需逐一确认。
    • -r

      • 将目录及以下之档案逐一删除。
  • 代码示例

    1
    2
    3
    4
    //删除文件可以直接使用rm命令,若删除目录则必须配合选项"-r"
    $ rm test.txt
    //删除当前目录下的所有文件及目录
    $ rm -r *
  • 注意事项

    文件一旦通过rm命令删除,则无法恢复,所以必须格外小心地使用该命令。


rsync——数据远程同步工具

  • 简介:

    • rsync命令广泛用于网络文件复制以及备份。
    • rsync可以在最小化数据传输量同时,同步不同位置上的文件和目录。
    • 相较于cp命令,rsync的优势在于比较文件修改日期,仅复制较新的文件。另外,它还支持远程数据传输以及压缩和加密。
  • 语法:

    • rsync [选项参数] [源文件路径] [目标文件路径]
  • 选项值:

    • -a

      • 进行归档操作
    • -v

      • 表示在stdout上打印出细节信息或进度
    • -z

      • 指定在传输时压缩数据
    • -r

      • 强制rsync以递归方式复制目录中所有的内容
    • –exclude

      • 指定不需要传输的文件,可以使用通配符指定需要排除的文件
      • $ rsync -avz /home/code/app /mnt/disk/backup/code --exclude "*.o"
    • –exclude-from

      • 我们也可以通过一个列表文件指定需要排除的文件
      • $ rsync -avz /home/code/app /mnt/disk/backup/code --exclude-from FILEPATH
    • –delete

      • 在更新rsync备份时,删除不存在的文件。默认情况下,rsync并不会在目的端删除那些在源端已不存在的文件。如果要删除这类文件,可以使用rsync的–delete选项:
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //将源目录(本地路径)复制到目的路径(远程路径)
    $ rsync -av /home/slynux/data slynux@192.168.0.6:/home/backups/data
    // 上面的命令会以递归的方式将所有的文件从源路径复制到目的路径。源路径和目的路径既可以是远程路径,也可以是本地路径。

    //以将远程主机上的数据恢复到本地
    $ rsync -av slynux@192.168.0.6:/home/backups/data /home/slynux/data

    //将一个目录中的内容同步到另一个目录,这条命令将源目录(/home/test)中的内容(不包括目录本身)复制到现有的backups目录中。
    $ rsync -av /home/test/ /home/backups

    //将包括目录本身在内的内容复制到另一个目录中,将包括源目录本身(/home/test)在内的内容复制到新的backups目录中
    $ rsync -av /home/test /home/backups
  • 注意事项

    rsync命令用SSH连接远程主机,因此必须使用username@host:PATH 这种形式设定远程主机的地址,其中user代表用户名,host代表远程主机的IP地址或主机名。而PATH指定了远程主机中待复制数据所在的路径。


S

scp——安全复制工具

  • 简介:

    • SCP是一个安全的文件复制命令,和旧式的、不安全的远程复制命令rcp类似。文件均通过SSH加密通道进行传输。
    • 因为是基于ssh,远程地址格式同样为username@host:/path
  • 语法:

    • scp [选项参数] [源文件路径] [目标文件路径]
  • 选项值:

    • -oPort=PORTNO

      • SSH服务器有时候并不在默认的端口22上运行。如果它在其他端口运行,我们可以在scp中用选项-oPort=PORTNO来指定端口号。
    • -1

      • 强制scp命令使用协议ssh1
    • -2

      • 强制scp命令使用协议ssh2
    • -4

      • 强制scp命令只使用IPv4寻址
    • -6

      • 强制scp命令只使用IPv6寻址
    • -B

      • 使用批处理模式(传输过程中不询问传输口令或短语)
    • -C

      • 允许压缩。(将-C标志传递给ssh,从而打开压缩功能)
    • -p

      • 保留原文件的修改时间,访问时间和访问权限。
    • -q

      • 不显示传输进度条。
    • -r

      • 递归复制整个目录。
    • -v

      • 详细方式显示输出。scp和ssh(1)会显示出整个过程的调试信息。这些信息用于调试连接,验证和配置问题。
    • -c cipher

      • 以cipher将数据传输进行加密,这个选项将直接传递给ssh。
    • -F ssh_config

      • 指定一个替代的ssh配置文件,此参数直接传递给ssh。
    • -i identity_file

      • 从指定文件中读取传输时使用的密钥文件,此参数直接传递给ssh。
    • -l limit

      • 限定用户所能使用的带宽,以Kbit/s为单位。
    • -o ssh_option

      • 如果习惯于使用ssh_config(5)中的参数传递方式,
    • -P port

      • 注意是大写的P, port是指定数据传输用到的端口号
    • -S program

      • 指定加密传输时所使用的程序。此程序必须能够理解ssh(1)的选项。
  • 代码示例

    1
    2
    //将远程主机中的文件复制到当前目录并使用给定的文件名
    $ scp user@remotehost:/home/path/filename filename
  • 注意事项


sed——对文本进行编辑

  • 简介:

  • sed是stream editor(流编辑器)的缩写。sed 可依照脚本的指令来处理、编辑文本文件。主要用来自动编辑一个或多个文件、简化对文件的反复操作、编写转换程序等。

  • 语法:

    • sed [选项参数][文本文件]
  • 选项值:

    • -e或–expression=

      • 以选项中指定的action动作来处理输入的文本文件。-e可省略,sed默认为该模式

      • action动作包括:

        • a :新增, a 的后面可以接字串,而这些字串会在新的一行出现(目前的下一行)~
        • c :取代行, c 的后面可以接字串,这些字串可以取代指定的行!
        • d :删除,因为是删除啊,所以 d 后面通常不接任何字符
        • i :插入, i 的后面可以接字串,而这些字串会在新的一行出现(目前的上一行);
        • p :打印,亦即将某个选择的数据印出。通常 p 会与参数 sed -n 一起运行~
        • s :取代部分数据,可以直接进行取代的工作!通常s的动作可以搭配正规表示法!例如 1,20s/old/new/g

        注意动作符号后面,需要使用\符号分隔开动作与字符

    • -f<script文件>或–file=<script文件>

    • 以选项中指定的script文件(文件内容为action)来处理输入的文本文件。

  • -h或–help

    • 显示帮助。

    • -n或–quiet或–silent

    • 仅显示script处理后的结果。

    • -V或–version

      • 显示版本信息。
    • -i

      • in place的意思,在参数为文件时,选项-i会使得sed用修改后的数据替换原始文件,相当于直接修改文件
        $ sed -i 's/text/replace/' file
        直接修改文件十分危险,我们可以使用如下命令,这时的sed不仅替换文件内容,还会创建一个名为file.bak的文件,其中包含着原始文件内容的副本。
        sed -i.bak 's/text/replace/' file
  • 代码示例

    • a 添加

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      //在testfile文件的第一行后添加一行,并将结果输出到标准输出
      $ cat testfile #查看testfile 中的内容
      > HELLO LINUX!
      > Linux is a free unix-type opterating system.

      $ sed -e 1a\newLine testfile
      或
      $ sed -e '1a newLine' testfile
      > HELLO LINUX! #testfile文件原有的内容
      > newline
      > Linux is a free unix-type opterating system.
    • d 添加

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      //将 /etc/passwd 的内容列出,同时,请将第 2~5 行删除!(nl命令,给文本内容添加行号)
      $ nl /etc/passwd | sed '2,5d'
      > 1 root:x:0:0:root:/root:/bin/bash
      > 6 sync:x:5:0:sync:/sbin:/bin/sync
      > 7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
      > .....(后面省略).....

      //将 /etc/passwd 的内容列出,同时,请将第 2 行删除!
      $ nl /etc/passwd | sed '2d'

      //将 /etc/passwd 的内容列出,同时,要删除第 3 到最后一行
      $ nl /etc/passwd | sed '3,$d'

      //将 /etc/passwd 的内容列出,要在第二行前加上drink tea
      $ cat /etc/passwd | sed '2i drink tea'

      //删除/etc/passwd所有包含root的行,其他行输出
      $ nl /etc/passwd | sed '/root/d'
      > 2 daemon:x:1:1:daemon:/usr/sbin:/bin/sh
      > 3 bin:x:2:2:bin:/bin:/bin/sh
      > ....下面忽略,第一行的匹配root已经删除了

      //删除空行
      $ sed '/^$/d' file
    • c 替换

      1
      2
      3
      4
      5
      6
      //将第2-5行的内容取代成为No 2-5 number
      $ nl /etc/passwd | sed '2,5c No 2-5 number'
      > 1 root:x:0:0:root:/root:/bin/bash
      > No 2-5 number
      > 6 sync:x:5:0:sync:/sbin:/bin/sync
      > .....(后面省略).....
    • p 打印

      1
      2
      3
      4
      5
      6
      7
      8
      9
      //仅列出 /etc/passwd 文件内的第 5-7 行
      $ nl /etc/passwd | sed -n '5,7p'
      > 5 lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
      > 6 sync:x:5:0:sync:/sbin:/bin/sync
      > 7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown

      //搜索 /etc/passwd有root关键字的行
      $ nl /etc/passwd | sed -n '/root/p'
      > 1 root:x:0:0:root:/root:/bin/bash
    • s 取代(语法:sed 's/正则表达式/新的字串/')

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      //替换第一个出现的this
      $ echo thisthisthisthis | sed 's/this/THIS/'
      > THISthisthisthis

      //g标记可以使sed执行全局替换。
      $ echo thisthisthisthis | sed 's/this/THIS/g'
      > THISTHISTHISTHIS

      ///#g标记可以使sed替换第N次起出现的匹配
      $ echo thisthisthisthis | sed 's/this/THIS/2g'
      > thisTHISTHISTHIS
      $ echo thisthisthisthis | sed 's/this/THIS/3g'
      > thisthisTHISTHIS

      //g标记可以使sed执行全局替换。
      $ echo thisthisthisthis | sed 's/this/THIS/g'
      > THISTHISTHISTHIS

      //搜索/etc/passwd,找到root对应的行,执行后面花括号中的一组命令。
      //每个命令之间用分号分隔,这里把bash替换为blueshell,再输出这行
      $ nl /etc/passwd | sed -n '/root/{s/bash/blueshell/;p;q}' #s为替换,p为打印,最后的q是退出。
      > 1 root:x:0:0:root:/root:/bin/blueshell

      //sed命令会将s之后的字符视为命令分隔符。这允许我们更改默认的分隔符/:
      $ sed 's:text:replace:g' #此时分隔符是:

      //如果作为分隔符的字符出现在模式中,必须使用\对其进行转义。\|是出现在模式中被转义的分隔符
      $ sed 's|te\|xt|replace|g'

      //在sed中,我们可以用&指代模式所匹配到的字符串,这样就能够在替换字符串时使用已匹配的内容
      //在这个例子中,正则表达式\w\+匹配每一个单词,然后我们用[&]替换它。&对应于之前所匹配到的单词。
      $ echo this is an example | sed 's/\w\+/[&]/g'
      > [this] [is] [an] [example]
  • 注意事项

    可以利用管道组合多个sed命令,多个模式之间可以用分号分隔,或是使用选项-e PATTERN ;如下命令都是等价的:
    sed 'expression' | sed 'expression'
    sed 'expression; expression'
    sed -e 'expression' -e 'expression'

    如果想在sed表达式中使用变量,双引号就能派上用场了。
    $ text=hello
    $ echo hello world | sed “s/$text/HELLO/“
    HELLO world


set-调试执行命令

  • 简介:

  • set命令能设置所使用shell的执行方式,可依照不同的需求来做设置。

  • 语法:

    • set [+-abCdefhHklmnpPtuvx]
  • 选项值:

    • -a

      •  标示已修改的变量,以供输出至环境变量。
    • -b

      •  使被中止的后台程序立刻回报执行状态。
    • -C

      •  转向所产生的文件无法覆盖已存在的文件。
    • -d

      •  Shell预设会用杂凑表记忆使用过的指令,以加速指令的执行。使用-d参数可取消。
    • -e

      •  若指令传回值不等于0,则立即退出shell。
    • -f

      •  取消使用通配符。
    • -h

      •  自动记录函数的所在位置。
    • -k

      •  指令所给的参数都会被视为此指令的环境变量。
    • -l

      •  记录for循环的变量名称。
    • -m

      •  使用监视模式。
    • -n

      •  只读取指令,而不实际执行。
    • -p

      •  启动优先顺序模式。
    • -P

      •  启动-P参数后,执行指令时,会以实际的文件或目录来取代符号连接。
    • -t

      •  执行完随后的指令,即退出shell。
    • -u

      •  当执行时使用到未定义过的变量,则显示错误信息。
    • -v

      •  显示shell所读取的输入值。
    • -x

      •  执行指令后,会先显示该指令及参数。
    • +<参数>

      •  取消某个set曾启动的参数。
  • 注意事项

    +<参数>:取消某个set曾启动的参数。

    set命令不带任何参数时,为打印所有环境变量

  • 代码示例

    1
    2
    3
    4
    5
    //执行指令后,会先显示该指令及参数。
    $ #!/bin/bash
    - #文件名: debug.sh
    - for i in {1..2};
    - do
    • set -x

    • echo $i

      • set +x
      • done
      • echo 1
        1
      • set +x
      • echo 2
        2
      • set +x
      1
      	

sftp——运行在SSH连接之上并模拟了FTP接口的文件传输系统

  • 简介:

    • 它不需要远端运行FTP服务器来进行文件传输,但必须要有SSH服务器。sftp是一个交互式命令,提供了命令提示符。
    • sftp支持与ftp和lftp相同的命令。不赘述。
  • 语法:

    • xxxxxxxxxxxxxxxxx
  • 选项值:

    • -oPort=PORTNO
      • SSH服务器有时候并不在默认的端口22上运行。如果它在其他端口运行,我们可以在sftp中用选项-oPort=PORTNO来指定端口号。
      • $ sftp -oPort=422 user@slynux.org
      • -oPort应该作为sftp命令的第一个参数。
  • 代码示例

    1
    2
    3
    //启动sftp会话
    $ sftp user@domainname
    >
  • 注意事项


sleep-命令延迟一段时间执行。

  • 简介:

  • Linux sleep命令可以用来将目前动作延迟一段时间。

  • 语法:

    • sleep [--help] [--version] number[smhd]
  • 选项值:

    • –help

      • 显示辅助讯息
    • –version

      • 显示版本编号
    • number

      • 时间长度,后面可接 s、m、h 或 d。其中 s 为秒,m 为 分钟,h 为小时,d 为日数
  • 代码示例

    1
    2
    //显示目前时间后延迟 1 分钟,之后再次显示时间
    $ date;sleep 1m;date

ssh——远程连接工具

  • 简介:

    • SSH代表的是Secure Shell(安全shell)。它使用加密隧道连接两台计算机。
    • SSH能够让你访问远程计算机上的shell,从而在其上执行交互命令并接收结果,或是启动交互会话。
    • GNU/Linux发布版中默认并不包含SSH,需要使用软件包管理器安装openssh-server和openssh-client。SSH服务默认运行在端口22之上。
  • 语法:

    • ssh [选项参数] [user@]hostname] [command]
  • 选项值:

    • -p <端口号>

      • SSH服务器默认在端口22上运行。但有些SSH服务器并没有使用这个端口。针对这种情况,可以用ssh命令的-p <端口号>来指定端口
      • ssh user@locahost -p 422
    • - -C

      • SSH协议也支持对数据进行压缩传输。当带宽有限时,这一功能很方便。
      • ssh -C user@hostname COMMANDS
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //连接运行了SSH服务器的远程主机
    $ ssh mec@192.168.0.1
    //mec是远程主机上的用户,192.168.0.1是IP地址,这里也可以是域名
    //SSH会询问用户密码,一旦认证成功,就会连接到远程主机上的登录shell

    //在远程执行命令whoami
    $ ssh mec@192.168.0.1 'whoami'
    > mec

    //可以输入多条命令,命令之间用分号分隔
    $ ssh mec@192.168.0.1 "echo user: $(whoami);echo OS: $(uname)"
    > user: mec
    > OS: Linux

    //将数据重定向至远程shell命令的stdin
    $ echo 'text' | ssh user@remote_host 'echo'
    > text

    //将本地主机上的tar存档文件传给远程主机
    $ > tar -czf - LOCALFOLDER | ssh 'tar -xzvf-'
  • 注意事项


T

tar——归档或者压缩,解压文件

  • 简介:

  • tar命令可以归档文件。它最初是设计用来将数据存储在磁带上,因此其名字也来源于TapeARchive。tar可以将多个文件和文件夹打包为单个文件,同时还能保留所有的文件属性,如所有者、权限等。

  • 语法:

    • tar [选项参数]
  • 选项值:

    • -A或–catenate

      • 新增文件到已存在的备份文件。
    • -b<区块数目>或–blocking-factor=<区块数目>

      • 设置每笔记录的区块数目,每个区块大小为12Bytes。
    • -B或–read-full-records

      • 读取数据时重设区块大小。
    • -c或–create

      • 建立新的备份文件。
    • -C<目的目录>或–directory=<目的目录>

      • 切换到指定的目录。
    • -d或–diff或–compare

      • 对比备份文件内和文件系统上的文件的差异。
    • -f<备份文件>或–file=<备份文件>

      • 指定备份文件。
    • -F<Script文件>或–info-script=<Script文件>

      • 每次更换磁带时,就执行指定的Script文件。
    • -g或–listed-incremental

      • 处理GNU格式的大量备份。
    • -G或–incremental

      • 处理旧的GNU格式的大量备份。
    • -h或–dereference

      • 不建立符号连接,直接复制该连接所指向的原始文件。
    • -i或–ignore-zeros

      • 忽略备份文件中的0 Byte区块,也就是EOF。
    • -k或–keep-old-files

      • 解开备份文件时,不覆盖已有的文件。
    • -K<文件>或–starting-file=<文件>

      • 从指定的文件开始还原。
    • -l或–one-file-system

      • 复制的文件或目录存放的文件系统,必须与tar指令执行时所处的文件系统相同,否则不予复制。
    • -m或–modification-time

      • 还原文件时,不变更文件的更改时间。
    • -M或–multi-volume

      • 在建立,还原备份文件或列出其中的内容时,采用多卷册模式。
    • -N<日期格式>或–newer=<日期时间>

      • 只将较指定日期更新的文件保存到备份文件里。
    • -o或–old-archive或–portability

      • 将资料写入备份文件时使用V7格式。
    • -O或–stdout

      • 把从备份文件里还原的文件输出到标准输出设备。
    • -p或–same-permissions

      • 用原来的文件权限还原文件。
    • -P或–absolute-names

      • 文件名使用绝对名称,不移除文件名称前的”/“号。
    • -r或–append

      • 新增文件到已存在的备份文件的结尾部分。
    • -R或–block-number

      • 列出每个信息在备份文件中的区块编号。
    • -s或–same-order

      • 还原文件的顺序和备份文件内的存放顺序相同。
    • -S或–sparse

      • 倘若一个文件内含大量的连续0字节,则将此文件存成稀疏文件。
    • -t或–list

      • 列出备份文件的内容。
    • -T<范本文件>或–files-from=<范本文件>

      • 指定范本文件,其内含有一个或多个范本样式,让tar解开或建立符合设置条件的文件。
    • -u或–update

      • 仅置换较备份文件内的文件更新的文件。追加选项(-r)可以将指定的任意文件加入到归档文件中。如果同名文件已经存在,那么归档文件中就会包含两个名字一样的文件。我们可以用更新选项-u指明:只添加比归档文件中的同
        名文件更新(newer)的文件。

      • $ tar -uf archive.tar filea #如果两个filea的时间戳相同,则什么都不会发生。

    • -U或–unlink-first

      • 解开压缩文件还原文件之前,先解除文件的连接。
    • -v或–verbose

      • 显示指令执行过程。
    • -V<卷册名称>或–label=<卷册名称>

      • 建立使用指定的卷册名称的备份文件。
    • -w或–interactive

      • 遭遇问题时先询问用户。
    • -W或–verify

      • 写入备份文件后,确认文件正确无误。
    • -x或–extract或–get

      • 从备份文件中还原文件。
    • -X<范本文件>或–exclude-from=<范本文件>

      • 指定范本文件,其内含有一个或多个范本样式,让ar排除符合设置条件的文件。
    • -z或–gzip或–ungzip

      • 通过gzip指令处理备份文件。
    • -Z或–compress或–uncompress

      • 通过compress指令处理备份文件。
    • -<设备编号><存储密度>

      • 设置备份用的外围设备编号及存放数据的密度。
    • –after-date=<日期时间>

      • 此参数的效果和指定”-N”参数相同。
    • –atime-preserve

      • 不变更文件的存取时间。
    • –backup=<备份方式>或–backup

      • 移除文件前先进行备份。
    • –checkpoint

      • 读取备份文件时列出目录名称。
    • –concatenate

      • 此参数的效果和指定”-A”参数相同。
    • –confirmation

      • 此参数的效果和指定”-w”参数相同。
    • –delete

      • 从备份文件中删除指定的文件。
    • –exclude=<范本样式>

      • 排除符合范本样式的文件。
    • –group=<群组名称>

      • 把加入设备文件中的文件的所属群组设成指定的群组。
    • –help

      • 在线帮助。
    • –ignore-failed-read

      • 忽略数据读取错误,不中断程序的执行。
    • –new-volume-script=<Script文件>

      • 此参数的效果和指定”-F”参数相同。
    • –newer-mtime

      • 只保存更改过的文件。
    • –no-recursion

      • 不做递归处理,也就是指定目录下的所有文件及子目录不予处理。
    • –null

      • 从null设备读取文件名称。
    • –numeric-owner

      • 以用户识别码及群组识别码取代用户名称和群组名称。
    • –owner=<用户名称>

      • 把加入备份文件中的文件的拥有者设成指定的用户。
    • –posix

      • 将数据写入备份文件时使用POSIX格式。
    • –preserve

      • 此参数的效果和指定”-ps”参数相同。
    • –preserve-order

      • 此参数的效果和指定”-A”参数相同。
    • –preserve-permissions

      • 此参数的效果和指定”-p”参数相同。
    • –record-size=<区块数目>

      • 此参数的效果和指定”-b”参数相同。
    • –recursive-unlink

      • 解开压缩文件还原目录之前,先解除整个目录下所有文件的连接。
    • –remove-files

      • 文件加入备份文件后,就将其删除。
    • –rsh-command=<执行指令>

      • 设置要在远端主机上执行的指令,以取代rsh指令。
    • –same-owner

      • 尝试以相同的文件拥有者还原文件。
    • –suffix=<备份字尾字符串>

      • 移除文件前先行备份。
    • –totals

      • 备份文件建立后,列出文件大小。
    • –use-compress-program=<执行指令>

      • 通过指定的指令处理备份文件。
    • –version

      • 显示版本信息。
    • –volno-file=<编号文件>

      • 使用指定文件内的编号取代预设的卷册编号。
  • 注意事项

    tar命令默认只归档文件,并不对其进行压缩。不过tar支持用于压缩的相关选项。我们日常理解的tar能压缩,其实都并非其命令的本意。压缩能够显著减少文件的体积。
    归档文件通常被压缩成下列格式之一。
    gzip格式:file.tar.gz或file.tgz。
    bzip2格式:file.tar.bz2。
    Lempel-Ziv-Markov格式:file.tar.lzma。

    不同的tar选项可以用来指定不同的压缩格式
    -j 指定bunzip2格式;
    -z 指定gzip格式;
    –lzma 指定lzma格式。

    不明确指定上面那些特定的选项也可以使用压缩功能。tar能够基于输出或输入文件的扩展名来进行压缩。为了让tar支持根据扩展名自动选择压缩算法,使用-a或–auto-compress选项
    $ tar -acvf archive.tar.gz filea fileb filec

  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    //归档文件,选项-c表示创建新的归档文件。选项-f表示归档文件名,该选项后面必须跟一个或多个文件
    $ tar -cf output.tar file
    $ $ tar -cf archive.tar file1 file2 file3 folder1

    //列出归档文件内容
    $ tar -tf archive.tar
    > file1
    > file2

    //列出压缩文件内容,而且还要展示更多的细节,使用v选项表示冗长模式,vv为非常冗长模式;
    $ tar -tvf archive.tar
    > -rw-rw-r-- shaan/shaan 0 2013-04-08 21:34 file1
    > -rw-rw-r-- shaan/shaan 0 2013-04-08 21:34 file2

    // 向归档文件中追加文件,选项-r可以将新文件追加到已有的归档文件末尾
    $ tar -tf archive.tar
    > hello.txt
    $ tar -rf archive.tar world.txt
    $ tar -tf archive.tar
    > hello.txt
    > world.txt

    //提取文件,-x可以将归档文件的内容提取到当前目录
    $ tar -xf archive.tar

    //解压文件
    $ tar -xzf archive.tar

    //我们也可以用选项-C来指定将文件提取到哪个目录:
    $ tar -xf archive.tar -C /path/to/extraction_directory

    //上述命令将归档文件的内容提取到指定目录中。它提取的是归档文件中的全部内容。
    //我们可以通过将文件名作为命令行参数来提取特定的文件,下述命令只提取file1和file4,忽略其他文件
    $ tar -xvf file.tar file1 file4

    //在归档时,我们可以将stdout指定为输出文件,这样另一个命令就可以通过管道来读取(作为stdin)并进行其他处理。
    //当通过安全shell(Secure Shell,SSH)传输数据时,这招很管用。
    $ tar cvf - files/ | ssh user@example.com "tar xv -C Documents/"
    //在上面的例子中,对files目录中的内容进行了归档并将其输出到stdout(由-指明),然后提取到远程系统中的Documents目录中。

    //我们可以用选项-A合并多个tar文件。
    //假设我们现在有两个tar文件:file1.tar和file2.tar。下面的命令可以将file2.tar的内容合并到file1.tar中
    $ tar -Af file1.tar file2.tar

    //从归档中删除文件
    $ tar -f archive.tar --delete file1 file2
    或
    $ tar --delete --file archive.tar [FILE LIST]

    //在归档过程中排除部分文件
    //选项--exclude [PATTERN]可以将匹配通配符模式的文件排除在归档过程之外。例如,排除所有的.txt文件
    //注意,模式应该使用双引号来引用,避免shell对其进行扩展。
    $ tar -cf arch.tar * --exclude "*.txt"

    //可以配合选项-X将需要排除的文件列表放入文件中
    $ cat list
    > filea
    > fileb
    $ tar -cf arch.tar * -X list
    //这样就把filea和fileb排除了。

tail–打印文件尾部内容

  • 简介:

  • tail命令用于输入文件中的尾部内容。tail命令默认在屏幕上显示指定文件的末尾10行。如果给定的文件不止一个,则在显示的每个文件前面加一个文件名标题。如果没有指定文件或者文件名为“-”,则读取标准输入。

  • 语法:

    • tail [选项][参数]
  • 选项值:

    • –retry

      • 即是在tail命令启动时,文件不可访问或者文件稍后变得不可访问,都始终尝试打开文件。使用此选项时需要与选项“——follow=name”连用;
    • -c 或–bytes=N

      • 输出文件尾部的N(N为整数)个字节内容;N值之前有一个”+”号,则从文件开头的第N项开始显示,而不是显示文件的最后N项。N值后面可以有后缀:b表示512,k表示1024,m表示1 048576(1M)。
    • -f <name/descriptor>或;–follow <name/descript>

      • 显示文件最新追加的内容。“name”表示以文件名的方式监视文件的变化。“-f”与“-fdescriptor”等效;
    • -F

      • 与选项“-follow=name”和“–retry”连用时功能相同;
    • -n或–line=N

      • 输出文件的尾部N(N位数字)行内容。N值之前有一个”+”号,则从文件开头的第N项开始显示,而不是显示文件的最后N项。
    • –pid=<进程号>

      • 与“-f”选项连用,当指定的进程号的进程终止后,自动退出tail命令;
    • -q或–quiet或–silent

      • 当有多个文件参数时,不输出各个文件名;
    • -s<秒数>或–sleep-interal=<秒数>

      • 与“-f”选项连用,指定监视文件变化时间隔的秒数;
    • -v或–verbose

      • 当有多个文件参数时,总是输出各个文件名;
    • –help

      • 显示指令的帮助信息;
    • –version

      • 显示指令的版本信息。
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //显示文件file的最后10行
    $ tail file

    //显示文件file的内容,从第20行至文件末尾
    $ tail +20 file

    //显示文件file的最后10个字符
    $ tail -c 10 file

    //实时查看file追加的内容
    $ tail -f file

    //间隔五秒刷新file追加的内容
    $ tail -f -s 5 file


time-量测命令执行消耗的时间

  • 简介:

  • Linux time命令的用途,在于量测特定指令执行时所需消耗的时间及系统资源等资讯。例如 CPU 时间、记忆体、输入输出等等。

  • 语法:

    • time [options] COMMAND [arguments]
  • 选项值:

    • -o 或 –output=FILE

      • 设定结果输出档。这个选项会将 time 的输出写入 所指定的档案中。如果档案已经存在,系统将覆写其内容。
    • -a 或 –append

      • 配合 -o 使用,会将结果写到档案的末端,而不会覆盖掉原来的内容。
    • -f 或 –format=FORMAT

      • 以 FORMAT 字串设定显示方式。当这个选项没有被设定的时候,会用系统预设的格式。不过你可以用环境变数 time 来设定这个格式,如此一来就不必每次登入系统都要设定一次。
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    //检测date命令运行时间和资源
    $ time date
    > real 0m0.136s
    > user 0m0.010s
    > sys 0m0.070s
    //在以上实例中,执行命令"time date"。系统先执行命令"date",第2行为命令"date"的执行结果。
    //第3-6行为执行命令"date"的时间统计结果,其中第4行"real"为实际时间,
    //第5行"user"为用户CPU时间,第6行"sys"为系统CPU时间。以上三种时间的显示格式均为MMmNN[.FFF]s。

tr-转换或删除文件中的字符。

  • 简介:

  • tr 可以对来自标准输入的内容进行字符替换、字符删除以及重复字符压缩,将结果输出到标准输出设备。tr是translate(转换)的简写,因为它可以将一组字符转换成另一组字符。

  • 语法:

    • tr [选项参数][--help][--version][第一字符集][第二字符集] 或 tr [OPTION] SET1[SET2]

      来自stdin的输入字符会按照位置从set1映射到set2(set1中的第一个字符映射到set2中的第一个字符,以此类推),然后将输出写入stdout(标准输出)。

      set1和set2是字符类或字符组。如果两个字符组的长度不相等,那么set2会不断复制其最后一个字符,直到长度与set1相同。如果set2的长度大于set1,那么在set2中超出set1长度的那部分字符则全部被忽略。

  • 选项值:

    • -c, –complement

      • 反选设定字符。也就是符合 SET1 的部份不做处理,不符合的剩余部份才进行转换
    • -d, –delete

      • 删除指令字符
    • -s, –squeeze-repeats

      • 缩减连续重复的字符成指定的单个字符
    • -t, –truncate-set1

      • 削减 SET1 指定范围,使之与 SET2 设定长度相等
    • –help

      • 显示程序用法信息
    • –version

      • 显示程序本身的版本信息
  • 字符集合的范围

    • \NNN

      • 八进制值的字符 NNN (1 to 3 为八进制值的字符)
    • \a Ctrl-G

      • 铃声
    • \b Ctrl-H

      • 退格符
    • \f Ctrl-L

      • 走行换页
    • \n Ctrl-J

      • 新行
    • \r Ctrl-M

      • 回车
    • \t Ctrl-I

      • tab键
    • \v Ctrl-X

      • 水平制表符
    • CHAR1-CHAR2

      • 字符范围从 CHAR1 到 CHAR2 的指定,范围的指定以 ASCII 码的次序为基础,只能由小到大,不能由大到小。
        ‘ABD-}’、’aA.,’、’a-ce-x’以及’a-c0-9’等均是合法的集合。
        定义集合也很简单,不需要书写一长串连续的字符序列,只需要使用“起始字符-终止字符”这种格式就行了。
    • [CHAR*]

      • 这是 SET2 专用的设定,功能是重复指定的字符到与 SET1 相同长度为止
    • [CHAR*REPEAT]

      • 这也是 SET2 专用的设定,功能是重复指定的字符到设定的 REPEAT 次数为止(REPEAT 的数字采 8 进位制计算,以 0 为开始)
    • [:alnum:]

      • 所有字母字符与数字
    • [:alpha:]

      • 所有字母字符
    • [:blank:]

      • 所有水平空格
    • [:cntrl:]

      • 所有控制(非打印)字符
    • [:digit:]

      • 所有数字
    • [:graph:]

      • 所有可打印的字符(不包含空格符)
    • [:lower:]

      • 所有小写字母
    • [:print:]

      • 所有可打印的字符(包含空格符)
    • [:punct:]

      • 所有标点字符
    • [:space:]

      • 所有水平与垂直空格符
    • [:upper:]

      • 所有大写字母
    • [:xdigit:]

      • 所有 16 进位制的数字
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    //文件testfile中的小写字母全部转换成大写字母,然后输出
    $ cat testfile |tr a-z A-Z
    或反过来
    $ echo "HELLO WHO IS THIS" |tr [:upper:] [:lower:]
    > hello who is this

    //用tr进行数字加密和解密
    //加密
    $ echo 12345 | tr '0-9' '9876543210'
    > 87654 # 已加密

    //解密
    $ echo 87654 | tr '9876543210' '0-9'
    > 12345 # 已解密

    //将制表符转换成单个空格
    $ tr '\t' ' ' < file.txt

    //用tr删除字符,语法上只使用set1,不使用set2
    //将stdin中的数字删除并打印删除后的结果
    $ echo "Hello 123 world 456" | tr -d '0-9'
    >

    //从输入文本中删除不在补集中的所有字符
    $ echo hello 1 char 2 next 4 | tr -d -c '0-9 \n'
    > 124

    //将不在set1中的字符替换成空格
    $ echo hello 1 char 2 next 4 | tr -c '0-9' ' '
    > 1 2 4

    //如果你习惯在点号后面放置两个空格,你需要在不删除重复字母的情况下去掉多余的空格(-s 缩减连续重复的字符成指定的单个字符)
    $ echo "GNU is not UNIX. Recursive right ?" | tr -s ' '
    > GNU is not UNIX. Recursive right ?

    //文件中的数字进行相加,先删除字母,再将' '替换成+号,得到 $[ 1+2+3+4+5 ],$[ operation ]执行算术运算,得出结果
    $ cat test.txt | tr -d [a-z] | echo "total: $[$(tr ' ' '+')]"
  • 注意事项

    tr只能通过stdin(标准输入)接收输入(无法通过命令行参数接收)


U

V

W

which-查找并显示给定命令的绝对路径

  • 简介:

  • 环境变量PATH中保存了查找命令时需要遍历的目录。which指令会在环境变量$PATH设置的目录里查找符合条件的文件。也就是说,使用which命令,就可以看到某个系统命令是否存在,以及执行的到底是哪一个位置的命令。

  • 语法:

    • which cmdName
  • 选项值:

  • 注意事项

    which是根据使用者所配置的 PATH 变量内的目录去搜寻可运行档的!所以,不同的 PATH 配置内容所找到的命令当然不一样的!

    cd 这种常用的命令找不到,因为 cd 是bash 内建的命令! 但是 which 默认是找 PATH 内所规范的目录,所以找不到的!

  • 代码示例

    1
    2
    3
    // 查找文件、显示命令路径
    $ which pwd
    > /bin/pwd

X

xargs-参数传递过滤器

  • 简介:

    • xargs 是给命令传递参数的一个过滤器,也是组合多个命令的一个工具。
    • xargs 可以将管道或标准输入(stdin)数据转换成命令行参数,也能够从文件的输出中读取数据。
    • xargs 也可以将单行或多行文本输入转换为其他格式,例如多行变单行,单行变多行。
    • xargs 默认的命令是 echo,这意味着通过管道传递给 xargs 的输入将会包含换行和空白,不过通过 xargs 的处理,换行和空白将被空格取代。
    • xargs 是一个强有力的命令,它能够捕获一个命令的输出,然后传递给另外一个命令。
    • 之所以能用到这个命令,关键是由于很多命令不支持|管道来传递参数,而日常工作中有有这个必要,所以就有了 xargs 命令,例如:
      1
      2
      find /sbin -perm +700 |ls -l       #这个命令是错误的
      find /sbin -perm +700 |xargs ls -l #这样才是正确的
  • 语法:

    • somecommand |xargs -item command
  • 选项值:

    • -a file

      • 从文件中读入作为sdtin
    • -e flag 或 -E flag

      • flag必须是一个以空格分隔的标志,当xargs分析到含有flag这个标志的时候就停止。
    • -p

      • 当每次执行一个argument的时候询问一次用户。
    • -n num

      • 后面加次数,表示命令在执行的时候一次用的argument的个数,默认是用所有的。如果用在输出场景,那就是一行有n个
    • -t

      • 表示先打印命令,然后再执行。
    • -i 或-I

      • 可以用于指定替换字符串,这个字符串会在xargs解析输入时被参数替换掉。如 复制所有图片文件到/data/images 目录下:-I 指定替换符号是{},那么xargs每个输出,都会替换cp后面的{}
        ls *.jpg | xargs -n1 -I {} cp {} /data/images
    • -r no-run-if-empty

      • 当xargs的输入为空的时候则停止xargs,不用再去执行了。
    • -s num

      • 命令行的最大字符数,指的是 xargs 后面那个命令的最大命令行字符数。
    • -L num

      • 从标准输入一次读取 num 行送给 command 命令。
    • -l

      • 同 -L。
    • -d delim

      • xargs对于输入的默认分隔符是空格,-d选项可以为输入数据指定自定义的分隔符
    • -x

      • exit的意思,主要是配合-s使用。。
    • -P

      • 修改最大的进程数,默认是1,为0时候为as many as it can
  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //多行输入单行输出
    $ cat test.txt
    > a b c d e f g
    > h i j k l m n

    $ cat test.txt | xargs
    > a b c d e f g h i j k l m n

    //-n 选项多行输出,每行三个元素
    $ cat test.txt | xargs -n 3
    > a b c
    > d e f
    > g h i

    //stdin中是一个包含了多个X字符的字符串。我们可以用–d选项将X定义为输入分隔符。
    $ echo "splitXsplit2Xsplit3Xsplit4" | xargs -d X
    > Split1 split2 split3 split4

    //在系统中搜索.docx文件,这些文件名中通常会包含大写字母和空格。其中使用了grep找出内容中不包含image的文件
    $ find /smbMount -iname '*.docx' -print0 | xargs -0 grep -L image
  • 注意事项

    xargs 一般是和管道一起使用。

    xargs命令可以同find命令很好地结合在一起。find的输出可以通过管道传给xargs,由后者执行-exec选项所无法处理的复杂操作。如果文件系统的有些文件名中包含空格,find命令的-print0选项可以使用0(NULL)来分隔查找到的元素,然后再用xargs对应的-0选项进行解析。


Y

Z

xxx

  • 简介:

    • xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • 语法:

    • xxxxxxxxxxxxxxxxx
  • 选项值:

  • 代码示例

    1
    2
    3
    //
    $
    >
  • 注意事项


shell notes&tips

Posted on 2019-07-17 | In 计算机协议和技术 , Linux相关 |
Words count in article: 8.1k | Reading time ≈ 31

tips

ps:下文中,我们使用$ 表示终端提示符表示输入命令的符号,- 表示多行命令的换行(多行命令不挤在一行以便美观),> 表示终端的输出。

右侧边有导航栏,可进行跳转


  • shell脚本通常以shebang起始,/bin/bash是Bash的解释器命令路径 #!/bin/bash

执行脚本

fork模式

我们所执行的任何程序,都是由父进程(parent process)所产生出来的一个子进程(child process),子进程在结束后,将返回到父进程去。此一现像在Linux系统中被称为 fork。当子进程被产生的时候,将会从父进程那里获得一定的资源分配、及(更重要的是)继承父进程的环境。

  • fork模式,脚本的执行方式有两种。

    • 将脚本名作为命令行参数(无须设置权限)

      $ bash myScript.sh

    • 授予脚本执行权限,将其变为可执行文件:

      1
      2
      $ chmod 755 myScript.sh
      $ ./myScript.sh.

      source模式

      source方式的特点是,在不另外创建子进程,而是在当前的的Shell环境中执行。

  • source模式,脚本的执行方式有两种。

    • source命令+文件路径

      $ source myScript.sh 或 source ./myScript.sh

    • . 命令

      $ . myScript.sh 或 . ./myScript.sh

exec模式

exec模式和source方式一样,不另外创建子进程,而是在当前的的Shell环境中执行脚本,但是执行完后会终止当前的shell进程,如果使用终端,可以看见执行exec后终端退出。

  • exec命令

    exec ./mytest.sh 或 exec myScript.sh


配置文件

你在命令行中输入的绝大部分命令都可以放置在一个特殊的文件中,留待登录或启动新的bash会话时执行。将函数定义、别名以及环境变量设置放置在这种特殊文件中,是一种定制shell的常用方法。

  • 当用户登录shell时,会执行下列文件:
    • /etc/profile
    • $HOME/.profile
    • $HOME/.bash_login
    • $HOME/.bash_profile

      注意,如果你是通过图形化登录管理器登入的话,是不会执行/etc/profile、$HOME/.profile和$HOME/.bash_profile这3个文件的。这是因为图形化窗口管理器并不会启动shell。当你打开终端窗口时才会创建shell,但这个shell也不是登录shell。

如果.bash_profile或.bash_login文件存在,则不会去读取.profile文件。

  • 交互式shell(如X11终端会话)或ssh执行单条命令(如ssh 192.168.1.1 ls /tmp)时,
    会读取并执行以下文件:
    • /etc/bash.bashrc
    • $HOME/.bashrc
  • 调用ssh登录会话
    ssh 192.168.1.100
    这会创建一个新的登录bash shell,该shell会读取并执行以下文件:
    • /etc/profile
    • /etc/bash.bashrc
    • $HOME/.profile
    • .bashrc_profile
  • 运行脚本
    1
    2
    3
    4
    //如果运行如下脚本:
    $> cat myscript.sh
    #!/bin/bash
    echo "Running"
    不会执行任何配置文件,除非定义了环境变量BASH_ENV:
    1
    2
    $> export BASH_ENV=~/.bashrc 
    $> ./myscript.sh

    变量

Shell变量大致可以分为3种类型:

  • 内部变量:系统提供,不用定义,不能修改,比如$#,$?,$*,$0等

  • 环境变量:系统提供,不用定义,可以修改,当前进程及其子进程中使用,比如PATH,PWD,SHELL等

  • 用户变量(本地变量):用户定义,可以修改,在当前进程使用,比如var=123等

    定义变量

    定义变量有如下几种形式

    • 不加符号的等号操作符赋值

    varName=value

    如果value不包含任何空白字符(例如空格),那么就不需要将其放入引号中,否则必须使用单引号或双引号。

    • 单引号的等号操作符赋值
      1
      2
      3
      4
      //单引号不扩展或解释任何变量和符号
      $ test='ps$?'
      $ echo $test
      > ps$?
    • 双引号的等号操作符赋值
      1
      2
      3
      4
      //双引号会扩展解释变量和符号,其中$?为上条命令执行的结果
      $ test="ps$?"
      $ echo $test
      > ps0
    • 反引号的等号操作符赋值
      1
      2
      3
      4
      //反引号(键盘上的~键),将内容命令的输出存入变量,该例中将ps命令的输出存入test
      $ test=`ps`
      $ echo $test
      PID TTY TIME CMD 2856 pts/0 00:00:00 bash 3234 pts/0 00:00:00 ps
    • 子shell的等号操作符赋值
      1
      2
      3
      4
      //使用$(),将开启子shell,或者说子进程执行内容命令,并将内容命令的输出存入变量,该例中将ps命令的输出存入test
      $ test=$(ps)
      $ echo $test
      PID TTY TIME CMD 2856 pts/0 00:00:00 bash 3234 pts/0 00:00:00 ps

      注意,var = value不同于var=value。两边没有空格的等号是赋值操作符,加上空格的等号表示的是等量关系测试。

    • export命令
      1
      2
      3
      4
      $ HTTP_PROXY=192.168.1.23:3128
      $ export HTTP_PROXY
      //export命令声明了将由子进程所继承的一个或多个变量。
      //这些变量被导出后,当前shell脚本所执行的任何应用程序都会获得这个变量。

      如果需要append变量,例如对PATH中添加一条新路径,可以使用如下命令:

      export PATH
      1
      2
      ### 常见的环境变量
      - SHELL:环境变量SHELL获知当前使用的是哪种shell

      $ echo $SHELL
      $ echo $0
      /bin/bash

      1
      2
      3
      4
      5
      - UID:环境变量UID中保存的是用户ID。root用户的UID是0。
      - PS1:当我们打开终端或是运行shell时,会看到类似于user@hostname:/home/$ 的提示字符串。不同的GNU/Linux发布版中的提示字符串及颜色各不相同。我们可以利用PS1环境变量来定义主提示字符串。
      - PATH:PATH环境变量通常保存了可用于搜索可执行文件的路径列表。`PATH=/usr/bin; /bin`这意味着只要shell执行应用程序(二进制文件或脚本)时,它就会首先查找/usr/bin,然后查找/bin。
      - LD_LIBRARY_PATH:LD_LIBRARY_PATH环境变量通常保存了可用于搜索库文件的路径列表。`LD_LIBRARY_PATH=/usr/lib; /lib`这意味着只要shell执行库文件时,它就会首先查找/usr/lib,然后查找/lib。
      - IFS:内部字段分隔符(internal field separator)。IFS环境变量保存了用于分隔的字符。它是当前shell环境使用的默认定界字符串。IFS的默认值为空白字符(换行符、制表符或者空格)。
      $ oldIFS=$IFS
    • IFS=, #IFS现在被设置为,
    • for item in $data;
    • do
    • echo Item: $item
    • done
    • IFS=$oldIFS

    Item: name
    Item: gender
    Item: rollno
    Item: location

    1
    2
    3
    - SHLVL:保存当前shell的层级
    ### 访问变量
    - 和编译型语言不同,大多数脚本语言不要求在创建变量之前声明其类型。用到什么类型就是什么类型。在变量名前面加上一个美元符号就可以访问到变量的值。也可以使用${var}。其分别如下:

    $ fruit=apple
    $ count=5
    $ echo “We have $count ${fruit}s”

    We have 5 apples
    //因为shell使用空白字符来分隔单词,
    //所以我们需要加上一对花括号来告诉shell这里的变量名是fruit,
    //而不是fruits。

    1
    2
    ### 获得字符串的长度
    - 可以用下面的方法获得变量值的长度:

    $ var=12345678901234567890
    $ echo $操作符

% %% # ## 操作符可以得到变量var删除特定的值后的结果:

假设我们定义file=/dir1/dir2/dir3/my.file.txt

可以用${ }分别替换得到不同的值:

  • ${file#*/}:删掉第一个 / 及其左边的字符串:dir1/dir2/dir3/my.file.txt
  • ${file##*/}:删掉最后一个 / 及其左边的字符串:my.file.txt
  • ${file#*.}:删掉第一个 . 及其左边的字符串:file.txt
  • ${file##*.}:删掉最后一个 . 及其左边的字符串:txt
  • ${file%/*}:删掉最后一个 / 及其右边的字符串:/dir1/dir2/dir3
  • ${file%%/*}:删掉第一个 / 及其右边的字符串:(空值)
  • ${file%.*}:删掉最后一个 . 及其右边的字符串:/dir1/dir2/dir3/my.file
  • ${file%%.*}:删掉第一个 . 及其右边的字符串:/dir1/dir2/dir3/my

记忆方法:

是去掉左边(键盘上#在 $ 的左边)去掉左边的时候,通配符*就要在指定的符号左边

% 是去掉右边(键盘上% 在$ 的右边)去掉右边的时候,通配符*就要在指定的符号右边
单一符号是最小匹配;吝啬匹配
两个符号是最大匹配;贪婪匹配


${::}操作符

  • ${file:0:5} :提取从第0个开始的连续5个字节:/dir1
  • ${file:5:5} :提取第5个开始的连续5个字节:/dir2

    ${//}

  • ${file/dir/path}:将第一个dir替换为path:/path1/dir2/dir3/my.file.txt
  • ${file//dir/path}:将全部dir 替换为 path:/path1/path2/path3/my.file.txt

    同样的:单一符号是最小匹配;吝啬匹配
    两个符号是最大匹配;贪婪匹配

    &&和||

  • shell 在执行某个命令的时候,会返回一个返回值,该返回值保存在 shell 变量 $? 中。当 $? == 0 时,表示执行成功;当 $? == 1 时(我认为是非0的数,返回值在0-255间),表示执行失败。
  • 有时候,下一条命令依赖前一条命令是否执行成功。如:在成功地执行一条命令之后再执行另一条命令,或者在一条命令执行失败后再执行另一条命令等。shell 提供了 && 和 || 来实现命令执行控制的功能,shell 将根据 && 或 || 前面命令的返回值来控制其后面命令的执行。
  • 无论是&&还是||,联合命令行都会尽量执行至成功为止。(这才有了短路的意义)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    command1 && command2 [&& command3 ...]
    //命令之间使用 && 连接,实现逻辑与的功能。
    //只有在 && 左边的命令返回真(命令返回值 $? == 0),&& 右边的命令才会被执行。
    //只要有一个命令返回假(命令返回值 $? == 1),后面的命令就不会被执行。

    command1 || command2 [|| command3 ...]
    //命令之间使用 || 连接,实现逻辑或的功能。
    //只有在 || 左边的命令返回假(命令返回值 $? == 1),|| 右边的命令才会被执行。这和 c 语言中的逻辑或语法功能相同,即实现短路逻辑或操作。
    //只要有一个命令返回真(命令返回值 $? == 0),后面的命令就不会被执行。 –直到返回真的地方停止执行。

    管道符 |

Unix shell脚本最棒的特性之一就是可以轻松地将多个命令组合起来生成输出。一个命令的输出可以作为另一个命令的输入,而这个命令的输出又会传递至下一个命令,以此类推。

1
2
//这里我们组合了3个命令。cmd1的输出传递给cmd2,cmd2的输出传递给cmd3,最终的输出 12(来自cmd3)会出现在显示器中或被导入某个文件。
$ cmd1 | cmd2 | cmd3

shell代码执行顺序

重定向执行顺序

  • 先读取输入重定向符<后的内容做为输入,如果一条命令有多个<,会读取最后一个<后的内容
    输入重定向符<放在命令前后都可以,例如【< /etc/hosts cat】相当于【cat /etc/hosts】

  • 执行命令

  • 如果有>或>>会将结果进行重定向,如果输出重定向多个文件,只会将内容重定向到最后一个文件
    例如 cat /etc/hosts > test1.txt >test2.txt,只有test2.txt会出现内容,test1.txt内容是空的
    输出重定向>和>>的位置放在哪里都可以,例如【> test.txt cat /etc/hosts】,表示将/etc/hosts的内容输入到test.txt中

管道符执行顺序

command1 | command2
命令1必须要有输出,且是正确的。命令2才会执行。命令1的输出作为命令2的输入


数学运算-let、(( ))和[]

Bash shell使用let、(( ))和[]执行基本的算术操作。工具expr和bc可以用来执行高级操作。

  • let命令
    • let命令可以直接执行基本的算术操作。当使用let时,变量名之前不需要再添加$,例如:
      1
      2
      3
      4
      5
      $ no1=4; 
      $ no2=5;
      $ let result=no1
      $ echo $result
      > 9
    • let自操作
      1
      2
      3
      4
      $ let no1++
      $ let no1--
      $ let no+=6
      $ let no-=6
  • 操作符$[]和$(())
    • 操作符[]的使用方法和let命令一样:
      1
      2
      3
      4
      5
      $ result=$[ no1 + no2 ]
      //在[]中也可以使用$前缀,例如:
      $ result=$[ $no1 + 5 ]
      //也可以使用操作符(())
      $ result=$(( no1 + 50 ))
  • expr
    • expr同样可以用于基本算术操作
      1
      2
      $ result=`expr 3 + 4`
      $ result=$(expr $no1 + 5)
  • bc
    • 上述命令不支持浮点数计算,浮点数计算需要使用bc命令,bc是一个用于数学运算的高级实用工具,这个精密的计算器包含了大量的选项。我们可以借助它执行浮点数运算并使用一些高级函数:
      1
      2
      3
      4
      5
      6
      $ echo "4 * 0.56" | bc
      > 2.24
      $ no=54;
      $ result=`echo "$no * 1.5" | bc`
      $ echo $result
      > 81.0
    • 设定小数精度。
      1
      2
      3
      //在下面的例子中,参数scale=2将小数位个数设置为2。因此,bc将会输出包含两个小数位的数值:
      $ echo "scale=2;22/7" | bc
      > 3.14
    • 进制转换
      1
      2
      3
      4
      5
      6
      7
      //用bc可以将一种进制系统转换为另一种。来看看下面的代码是如何在十进制与二进制之间相互转换的:
      $ no=100
      $ echo "obase=2;$no" | bc
      > 1100100
      $ no=1100100
      $ echo "obase=10;ibase=2;$no" | bc
      > 100
    • 计算平方以及平方根。
      1
      2
      $ echo "sqrt(100)" | bc #Square root 
      $ echo "10^10" | bc #Square

文件描述符与重定向

文件描述符是与输入和输出流相关联的整数。最广为人知的文件描述符是stdin、stdout和stderr。文件描述符0、1以及2是系统预留的。

  • 文件描述符

    1
    2
    3
    0 —— stdin (标准输入)。
    1 —— stdout(标准输出)。
    2 —— stderr(标准错误)。
  • 重定向符号

    符号 说明
    < file 输入重定向,将<后的file文件内容作为command执行前的输入
    > file 或1>file 输出重定向,将标准正确输出覆盖到后面的file文件内
    >> file或1>>file 输出重定向,将标准正确输出追加到后面的file文件内
    2>file 输出重定向,将标准错误输出覆盖到后面的file文件内
    2>>file 输出重定向,将标准错误输出追加到后面的file文件内
    &>file 或 >file 2>&1 输出重定向,将标准正确输出和标准错误输出覆盖到后面的file文件内
    &>>file 或 >>file 2>&1 输出重定向,将标准正确输出和标准错误输出追加到后面的file文件内

如果你不想看到或保存错误信息,那么可以将stderr的输出重定向到/dev/null,保证一切都
会被清除得干干净净。

cat<log.txt,注意,<<EOF是固定用法,<<不是指输入重定向两次,命令<<EOF会将键入的、以EOF输入字符为标准输入结束的流内容作为输入。然后按照重定向执行顺序,第二步骤执行cat命令,输出<<EOF键入的内容,最后将其重定向进log.txt


数组与关联数组

数组允许脚本利用索引将数据集合保存为独立的条目。Bash支持普通数组和关联数组,前者使用整数作为数组索引,后者使用字符串作为数组索引。当数据以数字顺序组织的时候,应该使用普通数组,例如一组连续的迭代。当数据以字符串组织的时候,关联数组就派上用场了,例如主机名称。

值序列

值序列在循环中经常使用,我们可以使用{1..5}来得到1-5的数字序列,也可以使用{1,2,3,4,5}得到同样的序列,也可以用{a..z}得到a-z的集合。

1
2
3
4
5
6
7
8
$ echo {1..5}
> 1 2 3 4 5

$ for i in {1,2,3,4,5,6};
- do
- echo $i
- done
> 1 2 3 4 5 6

普通数组

  • 定义数组

    • 可以在单行中使用数值列表来定义一个数组
      1
      2
      //这些值将会存储在以0为起始索引的连续位置上
      $ array_var=(test1 test2 test3 test4)
    • 定义特定索引数组值
      1
      $ array_var[2]="test3"
    • 定义空数组并加值
      1
      2
      3
      $ array_var=();
      //加入someVar变量值
      $ array_var+=("$someVar");
  • 访问数组

    • 访问特定索引的数组元素内容
      1
      2
      3
      4
      5
      $ echo ${array_var[0]} 
      > test1
      $ index=5
      $ echo ${array_var[$index]}
      > test6
    • 以列表形式打印出数组中的所有值
      1
      2
      3
      4
      5
      $ echo ${array_var[*]} 
      > test1 test2 test3 test4 test5 test6
      //或
      $ echo ${array_var[@]}
      > test1 test2 test3 test4 test5 test6
    • 打印数组长度(即数组中元素的个数)
      1
      $ echo ${#array_var[*]}

      关联数组

      关联数组从Bash 4.0版本开始被引入。当使用字符串(站点名、用户名、非顺序数字等)作为索引时,关联数组要比数字索引数组更容易使用。
  • 定义关联数组

    在关联数组中,我们可以用任意的文本作为数组索引。首先,需要使用声明语句将一个变量定义为关联数组

    1
    2
    $ declare -A fruits_value
    $ fruits_value=([apple]='100 dollars' [orange]='150 dollars')
  • 访问数组

    • 用下面的方法显示数组内容
      1
      2
      $ echo "Apple costs ${fruits_value[apple]}"
      > Apple costs 100 dollars
    • 列出数组索引(对于普通数组,这个方法同样可行。)
      1
      2
      3
      4
      5
      $ echo ${!fruits_value[*]}
      > orange apple
      //或
      $ echo ${!fruits_value[@]}
      > orange apple

函数

函数和别名乍一看很相似,不过两者在行为上还是略有不同。alias是使用纯文本代替命令名,它在命令解析阶段就会把内容进行替换,由于替换过程完全是基于文本的,因而别名可以改变shell的语法;

函数的函数体是复合命令(bash),函数名在命令解析阶段并不会被替换,只是在命令执行阶段调用相应的函数处理对应的复合命令。

函数参数可以在函数体中任意位置上使用,而别名只能将参数放在命令尾部。

定义函数

函数的定义包括function命令、函数名、开/闭括号以及包含在一对花括号中的函数体。

  • function 关键字
    1
    2
    3
    4
    function fname()
    {
    statements;
    }
  • 无 function 关键字
    1
    2
    3
    4
    5
    6
    fname()
    {
    statements;
    }
    或
    fname() { statement; }
  • 返回值
    在定义函数时,可以在函数体中使用return来定义返回值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    fname() 
    {
    if [ $1 -eq 0 ];
    then
    return 0; #返回值
    else
    return 1; #返回值
    fi
    }

    我们知道,在使用&&和||连接符时,判断依据即为符号前后命令的成功与否,返回值等于0为成功,大于0为失败。

调用函数

1
2
3
$ fname ; //执行函数
或者
$ fname arg1 arg2 ; //函数参数可以按位置访问,$1是第一个参数,$2是第二个参数,

函数体中,要活用$1,$2,$*,$@等符号

  • 递归调用

    1
    2
    3
    4
    fname() {
    echo $1; fname hello;
    sleep 1;
    }

    自定义函数示例

    自定义函数需要定义在rc或者profile文件中,此文件的作用可详见理解 bashrc 和 profile

  • 对于export PATH=/opt/myapp/bin:$PATH,我们可以在.bashrc文件中定义一个新的函数,来简化这一追加路径的功能,使得export PATH=/opt/myapp/bin:$PATH等价于prepend PATH /opt/myapp/bin,其中$1=PATH,$2=/opt/myapp/bin:

    1
    2
    3
    4
    5
    6
    7
    8
    prepend() { [ -d "$2" ] && eval $1=\"$2\$\{$1:+':'\$$1\}\" && export $1 ; }
    // [ -d "$2" ]含义为先确认该函数第二个参数所指定的目录是否存在。
    //如果存在,eval表达式将第一个参数所指定的变量值设置成第二个参数的值加上$\{$1:+':'\$$1\}表达式的值
    // $1=\"$2\$\{$1:+':'\$$1\}\"
    // $1="$2${PATH:+':'$PATH}" //$1=PATH有值
    // $1="$2':'$PATH"
    // $1="/opt/myapp/bin:$PATH"
    //如果第二步执行成功,第三步,export,完成

逻辑关键字

循环-for、while、until

  • 面向列表的for循环

    list可以是一个字符串,也可以是一个值序列。
    1
    2
    3
    4
    for var in {1..50};
    do
    commands;#使用变量$var
    done
  • 迭代指定范围的数字

    1
    2
    3
    4
    for((i=0;i<10;i++))
    {
    commands; #使用变量$i
    }
  • 循环到条件满足为止

    1
    2
    3
    4
    5
    6
    //当条件为真时,while循环继续执行;当条件不为真时,until循环继续执行。
    //用true或者:作为循环条件能够产生无限循环。
    while condition
    do
    commands;
    done
  • until循环

    1
    2
    3
    4
    5
    6
    //在Bash中还可以使用一个特殊的循环until。它会一直循环,直到给定的条件为真。例如:
    x=0;
    until [ $x -eq 9 ]; #条件是[$x -eq 9 ]
    do
    let x++; echo $x;
    done

    判断-if else

  • if条件

    1
    2
    3
    4
    if condition; 
    then
    commands;
    fi
  • else if和else

    1
    2
    3
    4
    5
    6
    7
    8
    if condition; 
    then
    commands;
    else if condition; then
    commands;
    else
    commands;
    fi

    if和else语句能够嵌套使用。if的条件判断部分可能会变得很长,但可以用
    逻辑运算符将它变得简洁一些:
    [ condition ] && action; # 如果condition为真,则执行action
    [ condition ] || action; # 如果condition为假,则执行action

判断条件- [] 和 [[]]

判断条件通常被放置在封闭的中括号内。一定要注意在 [ 和 ] 与操作数之间有一个空格。如果忘记了这个空格,脚本就会报错。[$var -eq 0 ] or [ $var -eq 0]会报错

在[]和[[ ]]中,其实任何一个符号两边都要有空格,所以在判断的时候,不要吝啬空格。

  • 对数字变量或值进行算术条件比较

    1
    2
    [ $var -eq 0 ] #当$var等于0时,返回真
    [ $var -ne 0 ] #当$var不为0时,返回真
    • 其他重要的操作符如下

      • -gt:大于。

      • -lt:小于。

      • -ge:大于或等于。

      • -le:小于或等于。

        这些操作符只适用于数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 文件系统相关判断

我们可以使用不同的条件标志测试各种文件系统相关的属性。
- [ -f $file_var ]:如果给定的变量包含正常的文件路径或文件名,则返回真。
- [ -x $var ]:如果给定的变量包含的文件可执行,则返回真。
- [ -d $var ]:如果给定的变量包含的是目录,则返回真。
- [ -e $var ]:如果给定的变量包含的文件存在,则返回真。
- [ -c $var ]:如果给定的变量包含的是一个字符设备文件的路径,则返回真。
- [ -b $var ]:如果给定的变量包含的是一个块设备文件的路径,则返回真。
- [ -w $var ]:如果给定的变量包含的文件可写,则返回真。
- [ -r $var ]:如果给定的变量包含的文件可读,则返回真。
- [ -L $var ]:如果给定的变量包含的是一个符号链接,则返回真
- [ -s $var ]:如果给定的变量包含的文件大小大于0字节,则返回真
- [ $var1 -nt $var2 ]:new than操作,如果给定的变量包含的文件1比文件2新,则返回真
- [ $var1 -ot $var2 ]:old than操作,如果给定的变量包含的文件1比文件2旧,则返回真
- [ $var1 -ef $var2 ]:equal file操作,如果给定的变量包含的文件1和文件2为同一文件,则返回真
- 字符串比较

**进行字符串比较时,最好用双中括号**,因为有时候采用单个中括号会产生错误。
- 测试两个字符串是否相同
//当str1等于str2时,返回真。也就是说,str1和str2包含的文本是一模一样的。 [[ $str1 = $str2 ]] //这是检查字符串是否相同的另一种写法。 [[ $str1 == $str2 ]] //如果str1和str2不相同,则返回真。 [[ $str1 != $str2 ]]
1
2
3
4
5
		> 注意在=前后各有一个空格。如果忘记加空格,那就不是比较关系了,而是变成了赋值语句。
- 字符串比较

字符串是依据字符的ASCII值进行比较的。例如,A的值是0x41,a的值是0x61。因此,A
小于a,AAa小于Aaa。
//如果str1的字母序比str2大,则返回真。 [[ $str1 > $str2 ]] //如果str1的字母序比str2小,则返回真。 [[ $str1 < $str2 ]]
1
- 判断空串
[[ -z $str1 ]] //如果str1为空串,则返回真。 [[ ! -z $str1 ]] //如果str1为空串,则返回假。与-n 等价 [[ -n $str1 ] //如果str1不为空串,则返回真。
1
2
3
4
5


- 逻辑与和逻辑或

- -a是逻辑与操作符,-o是逻辑或操作符。可以按照下面的方法结合多个条件进行
[ $var1 -ne 0 -a $var2 -gt 2 ] #使用逻辑与-a [ $var1 -ne 0 -o $var2 -gt 2 ] #逻辑或-o
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- [] 和 [[ ]]的区别

二者基本相等,除了下述几点

- 逻辑运算符不同
- []使用 -a 和 -o 来表达 与 和 或,不识别&& 和 ||
- [[ ]]使用 && 和 || 来表达 与 和 或,不识别-a 和 -o

- ==含义不同

- 在[[ ]]中,表达式"=="和"!="的右边其实会被当做pattern匹配,只不过不是正则匹配,是通配符匹配(即?表示匹配单个字符,*表示匹配零个一个或多个字符)。

- [[ ]]支持正则匹配

- 在[[ ]]中,表达式"=~"的右边会被当做正则匹配
- 不过要注意,使用"=~"时,右边表达式不需要引号,如:[[ '$var' =~ a* ]]

> 使用建议,无论是[]还是[[ ]],都建议对其变量使用双引号包围,换句话说,能做字符比较的时候,不要做数值比较。例如`var='shell script' [ $var = "shell script" ]` 会报错,因为变量不加双引号,相当于[ shell script = "shell script" ],这显然是错误的,所以应该加上引号`[ "$var" = "shell script" ]`

> 使用建议,使用-eq数值比较的时候,可以在操作符两边同时+0,避免变量为空报错,当日,一边为常数的话可以不用+0:` [ $((a+0)) -le 1]`

> test命令可以用来测试条件。用test可以避免使用过多的括号,增强代码的可读性。[]中的测试条件同样可以用于test命令。`if [ $var -eq 0 ]; then echo "True"; fi` 等价于 `if test $var -eq 0 ; then echo "True"; fi`
---
## Linux/unix文件系统
### 文件权限

文件权限和所有权是Unix/Linux文件系统的显著特性之一。这些特性能够在多用户环境中保护你的个人信息。每一个文件都拥有多种类型的权限。在这些权限中,我们通常要和三组权限打交道:用户、用户组以及其他用户。

用户(user)是文件的所有者,通常拥有所有的访问权。用户组(group)是多个用户的集合(由系统管理员指定),可能拥有文件的部分访问权。其他用户(others)是除文件所有者或用户组成员之外的任何人。

ls命令的-l选项可以显示出包括文件类型、权限、所有者以及组在内的多方面信息:

$ ls -l

-rw-r–r– 1 slynux users 2497 2010-02-28 11:22 bot.py
drwxr-xr-x 2 slynux users 4096 2010-05-27 14:31 a.py
-rw-r–r– 1 slynux users 539 2010-02-10 09:11 cl.pl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
上述代码中,第1列表明了文件类型。字符串slynux users分别对应用户和用户组。在这里,slynux是文件所有者,也是组成员之一。

其中文件类型有如下几种:
- -:普通文件。
- d:目录。
- c:字符设备。
- b:块设备。
- l:符号链接。
- s:套接字。
- p:管道

接下来的9个字符可以划分成三组,每组3个字符(--- --- ---)第一组的3个字符对应用户权限(所有者),第二组对应用户组权限,第三组对应其他用户权限。这9个字符(即9个权限)中的每一个字符指明是否其设置了某种权限。如果已设置,对应位置上会出现一个字符,否则出现一个-,表明没有设置对应的权限。

有3种常见的字符。

- r(read):如果设置,表明该文件、设备或目录可读。
- w(write):如果设置,表明该文件、设备或目录可以被修改。对于目录而言,此权限指定了是否可以在目录下创建或删除文件。
- x(execute):如果设置,表明该文件可执行。对于目录而言,此权限指定了能否访问目录下的文件。

同时这三组权限含义如下:

- 用户(权限序列:rwx------):定义了用户权限。通常来说,对于数据文件,用户权限是rw-;对于脚本或可执行文件,用户权限是rwx。用户还有一个称为setuid(S)的特殊权限,它出现在执行权限(x)的位置。setuid权限允许可执行文件以其拥有者的权限来执行,即使这个可执行文件是由其他用户运行的。具有setuid权限文件的权限序列可以是这样的:-rwS------。
- 用户组(权限序列:---rwx---):第二组字符指定了组权限。组权限中并没有setuid,但是有一个setgid(S)位。它允许使用与可执行文件所属组权限相同的有效组来运行该文件。但是这个组和实际发起命令的用户组未必相同。例如,组权限的权限序列可以是这样的:----rwS---。
- 其他用户(权限序列:------rwx):最后3个字符是其他用户权限。如果设置了相应的权限,其他用户也可以访问特定的文件或目录。作为一种规则,通常将这组权限设置为---。

> 目录有一个叫作粘滞位(sticky bit)的特殊权限。如果目录设置了粘滞位,只有创建该目录的用户才能删除目录中的文件,就算用户组和其他用户也有写权限,仍无能无力。粘滞位出现在其他用户权限组中的执行权限(x)位置。它使用t或T来表示。如果没有设置执行权限,但设置了粘滞位,就使用T;如果同时设置了执行权限和粘滞位,就使用t。例如:
> `------rwt , ------rwT`
> 设置目录粘滞位的一个典型例子就是/tmp,也就是说任何人都可以在该目录中创建文件,
> 但只有文件的所有者才能删除其所创建的文件。

> 可使用chmod命令设置文件权限。具体参见博文 [常用shell命令导航(Linux shell脚本攻略笔记)](https://my.oschina.net/u/4133922/blog/3077074 "常用shell命令导航(Linux shell脚本攻略笔记)")

## 有用的函数或者脚本

### 持续运行命令直至执行成功
有时候命令只有在满足某些条件时才能够成功执行。例如,在下载文件之前必须先创建该文件。这种情况下,你可能希望重复执行命令,直到成功为止。

//定义如下函数:
repeat()
{
while true
do
$@ && return
done
}
//函数repeat()中包含了一个无限while循环,该循环执行以函数参数形式(通过$@访问)传入的命令。如果命令执行成功,则返回,进而退出循环。

1
2
在大多数现代系统中,true是作为/bin中的一个二进制文件来实现的。
这就意味着每执行一次之前提到的while循环,shell就不得不生成一个进程。为了避免这种情况,可以使用shell的内建命令:,该命令的退出状态总是为0:

repeat() { while :; do $@ && return; done }

1
加入延时

//每30秒才会运行一次
repeat() { while :; do $@ && return; sleep 30; done }

---

git安装集成(Linux Ubuntu)

Posted on 2019-05-17 | In 工具&软件安装集成相关 |
Words count in article: 41 | Reading time ≈ 1

git 拥有官方的安装指导页面

1
https://git-scm.com/download/linux

ubuntu中我们使用
apt install git

安装完成后执行

git --version

得到

Gradle安装集成

Posted on 2019-05-16 | In 工具&软件安装集成相关 |
Words count in article: 1.2k | Reading time ≈ 4

安装

官网寻找资源 https://gradle.org/releases/ 红框部分得到下载链接

在Ubuntu环境下,使用
wget https://downloads.gradle.org/distributions/gradle-3.4.1-bin.zip
下载gradle

根据Gradle官方推荐,我们将安装包的内容解压至想要的路径(千万不要放在高权限路径)。
sudo unzip -d /home/lisheng/tools/gradle gradle-3.4.1-bin.zip

修改环境变量

sudo vim /etc/profile

1
2
PATH=$PATH:/home/lisheng/tools/gradle/gradle-3.4.1/bin
export PATH

配置生效

source /etc/profile

输入gradle -v检查gradle是否安全完成,以及查看其版本号。

使用,直接创建gradle项目即可

wrapper

当我们平时使用gradle来构建项目的时候,可以现在电脑上安装gradle,在配置环境变量之后就能正常使用了

不过当我们把项目分享给一个电脑上没安装gradle的人时,整体的项目构建还需要配置,显得麻烦。

由此就有了今天的主角:gradle wrapper 一个gradle的封装体,即使电脑上没有安装gradle也能构建。

初衷是因为gradle处于快速迭代阶段,经常发布新版本,如果我们的项目直接去引用,那么更改版本等会变得无比麻烦。而且每个项目又有可能用不一样的gradle版本,这样去手动配置每一个项目对应的gradle版本就会变得麻烦,gradle的引入本来就是想让大家构建项目变得轻松,如果这样的话,岂不是又增加了新的麻烦?所以android想到了包装,引入gradle-wrapper,通过读取配置文件中gradle的版本,为每个项目自动的下载和配置gradle,就是这么简单。

gradle建议开发者为每一个项目创建wrapper,以便其他人在没有gradle环境的机器上运行该项目。

如何创建wrapper呢?使用命令:

1
gradle wrapper

目录下会生成以下目录结构
Project-name/
gradlew
gradlew.bat
gradle/wrapper/
gradle-wrapper.jar
gradle-wrapper.properties

因为就像wrapper本身的意义,gradle命令行也是善变的,所以wrapper对命令行也进行了一层封装,使用同一的gradlew命令,wrapper会自动去执行具体版本对应的gradle命令。需要使用gradle wrapper的时候,我们就直接在项目根目录下直接执行gradlew(gradle wrapper的简写), 使用gradlew的方式和gradle一模一样, 例如通过gradlew tasks来查看所有的任务。事实上,执行gradlew命令的时候,gradlew会委托gradle命令来做相应的事情,所以gradlew真的只是一个壳而已。

当执行gradlew的时候,wrapper会检查当前机器是否已经安装了对应版本的gradle,如果安装了那么gradlew就会委托gradle执行用户输入的命令。如果还未安装的话,那么首先会自动帮我们从gradle repository下载安装。当然你也可以在配置文件中指定想要下载的server来替代默认的gradle repo。

那么我们如何去修改要下载的gradle版本呢?通过修改gradle-wrapper.properties文件

1
2
3
4
5
6
#Fri May 17 00:24:36 CST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-0.9-preview-1-bin.zip

distributionUrl:要下载的gradle地址以及版本,gradle-wrapper会去wrapper/list目录下查找,如果没有对应版本的gradle就调用url资源采取下载

gradle的3种版本:

  • gradle-xx-all.zip是完整版,包含了各种二进制文件,源代码文件,和离线的文档。例如,https://services.gradle.org/distributions/gradle-3.1-all.zip

  • gradle-xx-bin.zip是二进制版,只包含了二进制文件(可执行文件),没有文档和源代码。例如,https://services.gradle.org/distributions/gradle-3.1-bin.zip

  • gradle-xx-src.zip是源码版,只包含了Gradle源代码,不能用来编译你的工程。例如,https://services.gradle.org/distributions/gradle-3.1-src.zip

gradle-wrapper.properties各项属性的整体含义,如下:

  1. 去 https://services.gradle.org/distributions/gradle-3.1-bin.zip 下载gradle的3.1版本,只包含binary的版本。

  2. 下载的gradle-3.1-bin.zip存放到C:\Users<user_name>.gradle\wrapper\dists目录中。(注:具体还有2级目录,即全路径为C:\Users<user_name>.gradle\wrapper\dists\gradle-3.1-bin<url-hash>\,gradle-3.1-bin目录是根据下载的gradle的文件名来定的,目录是根据distribution url路径字符串计算md5值得来的

  3. 解压gradle-3.1-bin.zip,将解压后的文件存放到C:\Users<user_name>.gradle\wrapper\dists中。(注:具体还有2级目录,同上)

ElasticSearch升级记录 ver.1.4.5→ver.5.2.0

Posted on 2018-08-02 | In 中间件 , ElasticSearch |
Words count in article: 3.5k | Reading time ≈ 18

前言

项目中的es由ver.1.4.5升级至ver.5.2.0。

安装elasticSearch

1
2
3
4
#下载
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.2.0.tar.gz
# 解压
tar zxvf elasticsearch-5.5.0.tar.gz

修改elasticsearch.yml

1
$ES_HOME/config/elasticsearch.yml

在这里不详细展开elasticsearch.yml的各个配置项,附上链接。
配置es外部链接

安装elasticsearch-head

lasticsearch-head是一个很好的可视化前端框架,方便用可视化界面对es进行调用。elasticsearch-head在Github的地址如下:https://github.com/mobz/elasticsearch-head
安装也不复杂,由于它是一个前端的工具,因此需要我们预先安装了node和npm,之后执行下面的步骤:

1
2
3
git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install

安装完成后,运行命令npm run start就行。

调整弃用api的兼容问题

1.setting

1.4.5的org.elasticsearch.common.settings.ImmutableSettings已经弃用,生成配置对象setting的方式改成:

1
Settings settings = Settings.builder().put("cluster.name", clusterName).put("client.transport.sniff", true).build();

2.InetSocketTransportAddress

org.elasticsearch.common.transport.InetSocketTransportAddress#InetSocketTransportAddress(java.lang.String, int)方法已经弃用,注入集群地址的方式改成:

1
clusterNodeAddressList.add(new InetSocketTransportAddress(InetAddress.getByName(host), 9300));

3.TransportClient

org.elasticsearch.client.transport.TransportClient#TransportClient(org.elasticsearch.common.settings.Settings),该构造方法已经弃用,生成TransportClient实例的方式改成:

1
transportClient = new PreBuiltTransportClient(settings);

4.ClusterHealthStatus

org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus类已经弃用,相同功能由org.elasticsearch.cluster.health.ClusterHealthStatus继承

5.ScriptSortBuilder调整

原版写法:

1
2
3
4
5
6
7
Map<Object, Integer> optionalSortMap = manualSortMapBuilder.put("other", sortIndex + 1).build();

String script = "paramsMap.containsKey(doc[\"%s\"].value) ? paramsMap.get(doc[\"%s\"].value) : paramsMap.get('other')";

script = String.format(script, sort.getFieldName(), sort.getFieldName());

sortBuilder = SortBuilders.scriptSort(script, "number").param("paramsMap", optionalSortMap).order(SortOrder.ASC).missing(optionalSortMap.get("other"));

调整为:

1
2
3
4
5
6
7
8
9
10
11
12
Map<Object, Integer> optionalSortMap = manualSortMapBuilder.put("other", sortIndex + 1).build();

String script = "paramsMap.containsKey(doc[\"%s\"].value) ? paramsMap.get(doc[\"%s\"].value) : paramsMap.get('other')";

script = String.format(script, sort.getFieldName(), sort.getFieldName());
Map<String, Object> params = Maps.newConcurrentMap();

params.put("paramsMap", optionalSortMap);

Script scriptObject = new Script(Script.DEFAULT_SCRIPT_TYPE, Script.DEFAULT_SCRIPT_LANG, script, params);

sortBuilder = SortBuilders.scriptSort(scriptObject, ScriptSortBuilder.ScriptSortType.fromString("number")).order(SortOrder.ASC);

6.FilterBuilder调整

org.elasticsearch.index.query.FilterBuilder类已经弃用,基本上从2.x版本开始,Filter就已经弃用了(不包括bool查询内的filter),所有FilterBuilder全都要用QueryBuilder的各种子类来调整:

1.org.elasticsearch.index.query.BoolFilterBuilder

1
BoolFilterBuilder boolFilterBuilder = FilterBuilders.boolFilter();

调整为:

1
BoolQueryBuilder boolFilterBuilder = new BoolQueryBuilder();

2.org.elasticsearch.index.query.NestedFilterBuilder

1
filterBuilder = FilterBuilders.nestedFilter(param.getPath(), boolFilterBuilder);

调整为:

1
filterBuilder = new NestedQueryBuilder(param.getPath(), boolFilterBuilder, ScoreMode.None);

3.org.elasticsearch.index.query.MissingFilterBuilder

5.x版本中,missing关键字已经弃用,其功能由其逆运算exist继承。

1
2
3
4
5
6
7
MissingFilterBuilder missingFilterBuilder = FilterBuilders.missingFilter(paramName);
if (param.getNvlType() == QueryFieldType.EXISTS) {
filterBuilder = FilterBuilders.boolFilter().mustNot(missingFilterBuilder);
}
if (param.getNvlType() == QueryFieldType.MISSING) {
filterBuilder = FilterBuilders.boolFilter().must(missingFilterBuilder);
}

调整为:

1
2
3
4
5
6
7
ExistsQueryBuilder existsQueryBuilder = new ExistsQueryBuilder(paramName);
if (param.getNvlType() == QueryFieldType.EXISTS) {
filterBuilder = new BoolQueryBuilder().must(existsQueryBuilder);
}
if (param.getNvlType() == QueryFieldType.MISSING) {
filterBuilder = new BoolQueryBuilder().mustNot(existsQueryBuilder);
}

4.org.elasticsearch.index.query.TermFilterBuilder

1
filterBuilder = FilterBuilders.termFilter(paramName, param.getEqValue());

调整为:

1
filterBuilder = new TermQueryBuilder(paramName, param.getEqValue());

5.org.elasticsearch.index.query.TermsFilterBuilder

1
filterBuilder = FilterBuilders.inFilter(paramName, param.getInValues());

调整为:

1
filterBuilder = new TermsQueryBuilder(paramName, param.getInValues());

6.org.elasticsearch.index.query.RangeFilterBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//gte
if (null != param.getGteValue()) {
filterBuilder = FilterBuilders.rangeFilter(paramName).gte(param.getGteValue());
}
//gt
if (null != param.getGtValue()) {
filterBuilder = FilterBuilders.rangeFilter(paramName).gt(param.getGtValue());
}
//lte
if (null != param.getLteValue()) {
filterBuilder = FilterBuilders.rangeFilter(paramName).lte(param.getLteValue());
}
//lt
if (null != param.getLtValue()) {
filterBuilder = FilterBuilders.rangeFilter(paramName).lt(param.getLtValue());
}

调整为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//gte
if (null != param.getGteValue()) {
filterBuilder = new RangeQueryBuilder(paramName).gte(param.getGteValue());
}
//gt
if (null != param.getGtValue()) {
filterBuilder = new RangeQueryBuilder(paramName).gt(param.getGtValue());
}
//lte
if (null != param.getLteValue()) {
filterBuilder = new RangeQueryBuilder(paramName).lte(param.getLteValue());
}
//lt
if (null != param.getLtValue()) {
filterBuilder = new RangeQueryBuilder(paramName).lt(param.getLtValue());
}

7.search_type=count

原来我们想要计算文档的需要用到search_type=count,现在5.0已经将该API移除,取而代之你只需将size置于0即可:

1
2
3
4
5
6
7
8
9
10
GET /my_index/_search?search_type=count
{
"aggs": {
"my_terms": {
"terms": {
"field": "foo"
}
}
}
}

调整为:

1
2
3
4
5
6
7
8
9
10
11
12
#5.0以后
GET /my_index/_search
{
"size": 0,
"aggs": {
"my_terms": {
"terms": {
"field": "foo"
}
}
}
}

8.RangeBuilder

org.elasticsearch.search.aggregations.bucket.range.RangeBuilder已经弃用,相应功能由org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder实现,直接替换即可。

9.TopHitsAggregationBuilder

org.elasticsearch.search.aggregations.metrics.tophits.TopHitsBuilder已经弃用,相应功能由org.elasticsearch.search.aggregations.metrics.tophits.TopHitsAggregationBuilder实现,直接替换即可。

10.FiltersAggregationBuilder

org.elasticsearch.search.aggregations.bucket.filters.FiltersAggregationBuilder构造报文调整

1
2
3
4
5
6
7
FiltersAggregationBuilder filtersAggregationBuilder = AggregationBuilders.filters(aggregationField.getAggName());
LufaxSearchConditionBuilder tmpConditionBuilder = new LufaxSearchConditionBuilder();
for (String key : aggregationField.getFiltersMap().keySet()) {
LufaxFilterCondition tmpLufaxFilterCondition = aggregationField.getFiltersMap().get(key);
FilterBuilder tmpFilterBuilder = tmpConditionBuilder.constructFilterBuilder(tmpLufaxFilterCondition.getAndParams(),tmpLufaxFilterCondition.getOrParams(),tmpLufaxFilterCondition.getNotParams());
filtersAggregationBuilder.filter(key, tmpFilterBuilder);
}

调整成:

1
2
3
4
5
6
7
8
List<FiltersAggregator.KeyedFilter> keyedFilters = new LinkedList<FiltersAggregator.KeyedFilter>();
LufaxSearchConditionBuilder tmpConditionBuilder = new LufaxSearchConditionBuilder();
for (String key : aggregationField.getFiltersMap().keySet()) {
LufaxFilterCondition tmpLufaxFilterCondition = aggregationField.getFiltersMap().get(key);
QueryBuilder tmpFilterBuilder = tmpConditionBuilder.constructFilterBuilder(tmpLufaxFilterCondition.getAndParams(),tmpLufaxFilterCondition.getOrParams(),tmpLufaxFilterCondition.getNotParams());
keyedFilters.add(new FiltersAggregator.KeyedFilter(key, tmpFilterBuilder));
}
FiltersAggregationBuilder filtersAggregationBuilder = AggregationBuilders.filters(aggregationField.getAggName(), keyedFilters.toArray(new FiltersAggregator.KeyedFilter[]{}));

11.HighlightBuilder;

org.elasticsearch.search.highlight.HighlightBuilder弃用,相关功能由org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder实现。

12.OptimizeRequestBuilder

org.elasticsearch.action.admin.indices.optimize.OptimizeRequestBuilder 已经弃用,聚合索引的功能由org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequestBuilder来实现。

13.IndicesAliasesRequestBuilder

1.newAddAliasAction

旧版删除了AliasAction类的newAddAliasAction方法,故而IndicesAliasesRequestBuilder添加AliasActions应该:

1
requestBuilder.addAliasAction(AliasAction.newAddAliasAction(toIndex, indexAlias));

调整成

1
requestBuilder.addAliasAction(IndicesAliasesRequest.AliasActions.add().index(toIndex).alias(indexAlias));

2.newRemoveAliasAction

旧版删除了AliasAction类的newRemoveAliasAction方法,故而IndicesAliasesRequestBuilder删除AliasActions应该:

1
requestBuilder.addAliasAction(AliasAction.newRemoveAliasAction(fromIdx, indexAlias));

调整成

1
requestBuilder.addAliasAction(IndicesAliasesRequest.AliasActions.remove().index(fromIdx).alias(indexAlias));

14.AbstractAggregationBuilder的子类变更

1.org.elasticsearch.search.aggregations.bucket.terms.TermsBuilder

org.elasticsearch.search.aggregations.bucket.terms.TermsBuilder更名为
org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder

2.org.elasticsearch.search.aggregations.bucket.range.date.DateRangeBuilder

org.elasticsearch.search.aggregations.bucket.range.date.DateRangeBuilder更名为
org.elasticsearch.search.aggregations.bucket.range.date.DateRangeAggregationBuilder

3.org.elasticsearch.search.aggregations.metrics.tophits.TopHitsBuilder

org.elasticsearch.search.aggregations.metrics.tophits.TopHitsBuilder更名为
org.elasticsearch.search.aggregations.metrics.tophits.TopHitsAggregationBuilder

15.SearchHit类

org.elasticsearch.search.SearchHit#isSourceEmpty方法改为org.elasticsearch.search.SearchHit#hasSource方法,反向替换。

16.DeleteByQueryResponse

org.elasticsearch.action.deletebyquery.DeleteByQueryResponse已经弃用,

调整关键字等结构性问题

1. String数据类型弃用

在 ES2.x 版本字符串数据是没有 keyword 和 text 类型的,只有string类型,ES更新到5版本后,取消了 string 数据类型,代替它的是 keyword 和 text 数据类型。区别在于:

text类型定义的文本会被分析,在建立索引前会将这些文本进行分词,转化为词的组合,建立索引。允许 ES来检索这些词语。text 数据类型不能用来排序和聚合。

keyWord类型表示精确查找的文本,不需要进行分词。可以被用来检索过滤、排序和聚合。keyword 类型字段只能用本身来进行检索。

在没有显性定义时,es默认为“text”类型。

2. multi_field关键字弃用

相关mapping方式改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#对需要设置的字段,在'type'属性后增加"fields": 
#其中的"raw"为自定义的名称,想象它是city的一个分身。
PUT /my_index
{
"mappings": {
"my_type": {
"properties": {
"city": {
"type": "text",
"fields": {
"raw": {
"type": "keyword"
}
}
}
}
}
}
}

查询raw字段时,使用city.raw表示

3. analyzer

1.改版后,设置了search_analyzer的情况下,analyzer也要设置,否则会报:

1
analyzer on field [name] must be set when search_analyzer is set。

2.改版后,index_analyzer设置被弃用,如果设置,会报

1
MapperParsingException[Mapping definition for [fields] has unsupported parameters:  [index_analyzer : ik_max_word]];

这里扩展一下,在原来的版本中,index_analyzer负责建立索引时的分词器定义,search_analyzer负责搜索时的分词器定义。

索引期间查找解析器的完整顺序是这样的:

  • 定义在字段映射中的index_analyzer
  • 定义在字段映射中的analyzer
  • 定义在文档_analyzer字段中的解析器
  • type的默认index_analyzer
  • type的默认analyzer
  • 索引设置中default_index对应的解析器
  • 索引设置中default对应的解析器
  • 节点上default_index对应的解析器
  • 节点上default对应的解析器
  • standard解析器

而查询期间的完整顺序则是:

  • 直接定义在查询中的analyzer
  • 定义在字段映射中的search_analyzer
  • 定义在字段映射中的analyzer
  • type的默认search_analyzer
  • type的默认analyzer
  • 索引设置中的default_search对应的解析器
  • 索引设置中的default对应的解析器
  • 节点上default_search对应的解析器
  • 节点上default对应的解析器
  • standard解析器

现在新版删除index_analyzer,具体功能由analyzer关键字承担,analyzer关键字生效与index时和search时(除非search_analyzer已经被显性定义)。

3. _timestamp在2.0弃用

_timestamp官方建议自定义一个字段,自己赋值用来表示时间戳。

4. 嵌套字段排序时字段名称调整

对于如下的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PUT /my_index/blogpost/2
{
"title": "Investment secrets",
"body": "What they don't tell you ...",
"tags": [ "shares", "equities" ],
"comments": [
{
"name": "Mary Brown",
"comment": "Lies, lies, lies",
"age": 42,
"stars": 1,
"date": "2014-10-18"
},
{
"name": "John Smith",
"comment": "You're making it up!",
"age": 28,
"stars": 2,
"date": "2014-10-16"
}
]
}

老版本中,对stars字段进行排序时,直接可以

1
2
3
4
5
6
7
8
"sort" : [
{
"stars" : {
"order" : "desc",
"mode" : "min",
"nested_path" : "comments"
}
]

但在新版中,上述报文会报

1
No mapping found for [stars] in order to sort on

需要改成:

1
2
3
4
5
6
7
"sort" : [
{
"comments.stars" : {
"order" : "desc",
"mode" : "min"
}
]

5. _script脚本参数名变更

老版中,_script可以这样定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"sort" : [
{
"_script" : {
"script" : {
"inline" : "paramsMap.containsKey(doc[\"id\"].value) ? params.paramsMap.get(doc[\"id\"].value) : params.paramsMap.get('other')",
"lang" : "painless",
"params" : {
"paramsMap" : {
"1" : 1,
"2" : 1,
"3" : 2,
"other" : 3
}
}
},
"type" : "number",
"order" : "asc"
}
}
]

新版中,对于params的参数paramsMap必须用params.paramsMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"sort" : [
{
"_script" : {
"script" : {
"inline" : "params.paramsMap.containsKey(doc[\"productCategory\"].value) ? params.paramsMap.get(doc[\"productCategory\"].value) : params.paramsMap.get('other')",
"lang" : "painless",
"params" : {
"paramsMap" : {
"901" : 1,
"902" : 1,
"701" : 2,
"other" : 3
}
}
},
"type" : "number",
"order" : "asc"
}
}
]

注意:es 5.2.0默认禁用了动态语言,所以lang为painless之外的语言,默认情况下会报

1
ScriptException[scripts of type [inline], operation [update] and lang [groovy] are disabled];

需要在yml文件中添加配置(如groovy):

1
2
3
script.engine.groovy.inline:true 
script.engine.groovy.stored.search:true
script.engine.groovy.stored.aggs:true

6 .获取特定字段返回

在旧版本中,获取特定文档特定字段返回,可以使用stored_fields:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"from" : 0,
"size" : 1,
"query" : {},
"stored_fields" : "timestamp",
"sort" : [
{
"timestamp" : {
"order" : "desc"
}
}
]
}

新版本中,引入了更为强大的_source过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"from" : 0,
"size" : 1,
"query" : {},
"_source" : "timestamp",
"sort" : [
{
"timestamp" : {
"order" : "desc"
}
}
]
}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

{
"from" : 0,
"size" : 1,
"query" : {},
"_source" : {
"includes" : [ "timestamp" ],
"excludes" : [ "" ]
},
"sort" : [
{
"timestamp" : {
"order" : "desc"
}
}
]
}

java的api主要调用SearchRequestBuilder的setFetchSource方法

7. date字段的format定义

改版后,date字段最好再mapping时定义好format信息,以防止在请求前后因为格式转换问题报错:

1
ElasticsearchParseException[failed to parse date field [Thu Jun 18 00:00:00 CST 2015] with format [strict_date_optional_time||epoch_millis]]; nested: IllegalArgumentException[Parse failure at index [0] of [Thu Jun 18 00:00:00 CST 2015]]; }

[strict_date_optional_time||epoch_millis]是es默认的date字段解析格式

8. UncategorizedExecutionException

改版前,transport client发送数据之前将java代码中的字段序列化成了json然后进行传输和请求,而在5.x以后,es改用使用的内部的transport protocol,这时候,如果定义一个比如bigDecimal类型,es不支持bigDecimal,数据类型不匹配会抛错误。

1
UncategorizedExecutionException[Failed execution]; nested: IOException[can not write type [class java.math.BigDecimal]];

es支持的格式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
static {
Map<Class<?>, Writer> writers = new HashMap<>();
writers.put(String.class, (o, v) -> {
o.writeByte((byte) 0);
o.writeString((String) v);
});
writers.put(Integer.class, (o, v) -> {
o.writeByte((byte) 1);
o.writeInt((Integer) v);
});
writers.put(Long.class, (o, v) -> {
o.writeByte((byte) 2);
o.writeLong((Long) v);
});
writers.put(Float.class, (o, v) -> {
o.writeByte((byte) 3);
o.writeFloat((float) v);
});
writers.put(Double.class, (o, v) -> {
o.writeByte((byte) 4);
o.writeDouble((double) v);
});
writers.put(Boolean.class, (o, v) -> {
o.writeByte((byte) 5);
o.writeBoolean((boolean) v);
});
writers.put(byte[].class, (o, v) -> {
o.writeByte((byte) 6);
final byte[] bytes = (byte[]) v;
o.writeVInt(bytes.length);
o.writeBytes(bytes);
});
writers.put(List.class, (o, v) -> {
o.writeByte((byte) 7);
final List list = (List) v;
o.writeVInt(list.size());
for (Object item : list) {
o.writeGenericValue(item);
}
});
writers.put(Object[].class, (o, v) -> {
o.writeByte((byte) 8);
final Object[] list = (Object[]) v;
o.writeVInt(list.length);
for (Object item : list) {
o.writeGenericValue(item);
}
});
writers.put(Map.class, (o, v) -> {
if (v instanceof LinkedHashMap) {
o.writeByte((byte) 9);
} else {
o.writeByte((byte) 10);
}
@SuppressWarnings("unchecked")
final Map<String, Object> map = (Map<String, Object>) v;
o.writeVInt(map.size());
for (Map.Entry<String, Object> entry : map.entrySet()) {
o.writeString(entry.getKey());
o.writeGenericValue(entry.getValue());
}
});
writers.put(Byte.class, (o, v) -> {
o.writeByte((byte) 11);
o.writeByte((Byte) v);
});
writers.put(Date.class, (o, v) -> {
o.writeByte((byte) 12);
o.writeLong(((Date) v).getTime());
});
writers.put(ReadableInstant.class, (o, v) -> {
o.writeByte((byte) 13);
final ReadableInstant instant = (ReadableInstant) v;
o.writeString(instant.getZone().getID());
o.writeLong(instant.getMillis());
});
writers.put(BytesReference.class, (o, v) -> {
o.writeByte((byte) 14);
o.writeBytesReference((BytesReference) v);
});
writers.put(Text.class, (o, v) -> {
o.writeByte((byte) 15);
o.writeText((Text) v);
});
writers.put(Short.class, (o, v) -> {
o.writeByte((byte) 16);
o.writeShort((Short) v);
});
writers.put(int[].class, (o, v) -> {
o.writeByte((byte) 17);
o.writeIntArray((int[]) v);
});
writers.put(long[].class, (o, v) -> {
o.writeByte((byte) 18);
o.writeLongArray((long[]) v);
});
writers.put(float[].class, (o, v) -> {
o.writeByte((byte) 19);
o.writeFloatArray((float[]) v);
});
writers.put(double[].class, (o, v) -> {
o.writeByte((byte) 20);
o.writeDoubleArray((double[]) v);
});
writers.put(BytesRef.class, (o, v) -> {
o.writeByte((byte) 21);
o.writeBytesRef((BytesRef) v);
});
writers.put(GeoPoint.class, (o, v) -> {
o.writeByte((byte) 22);
o.writeGeoPoint((GeoPoint) v);
});
WRITERS = Collections.unmodifiableMap(writers);
}

线程池源码分析--ThreadPoolExecutor

Posted on 2018-03-27 | In JAVA , JAVA线程与并发控制 |
Words count in article: 13.5k | Reading time ≈ 58

序言

我们知道,线程池帮我们重复管理线程,避免创建大量的线程增加开销。
合理的使用线程池能够带来3个很明显的好处:

  1. 降低资源消耗:通过重用已经创建的线程来降低线程创建和销毁的消耗
  2. 提高响应速度:任务到达时不需要等待线程创建就可以立即执行。
  3. 提高线程的可管理性:线程池可以统一管理、分配、调优和监控。
    java源生的线程池,实现于ThreadPoolExecutor类,这也是我们今天讨论的重点

    1. ThreadPoolExecutor类构造方法

    Jdk使用ThreadPoolExecutor类来创建线程池,我们来看看它的构造方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
  • int corePoolSize, //核心线程的数量

  • int maximumPoolSize, //最大线程数量

  • long keepAliveTime, //超出核心线程数量以外的线程空闲时,线程存活的时间

  • TimeUnit unit, //存活时间的单位,有如下几种选择

    1
    2
    3
    4
    5
    6
    7
    TimeUnit.DAYS;               //天
    TimeUnit.HOURS; //小时
    TimeUnit.MINUTES; //分钟
    TimeUnit.SECONDS; //秒
    TimeUnit.MILLISECONDS; //毫秒
    TimeUnit.MICROSECONDS; //微妙
    TimeUnit.NANOSECONDS; //纳秒
  • BlockingQueue workQueue, //保存待执行任务的队列,常见的也有如下几种:

    1
    2
    3
    4
    5
    ArrayBlockingQueue;
    LinkedBlockingQueue;
    SynchronousQueue;
    PriorityBlockingQueue
    ...

  • ThreadFactory threadFactory, //创建新线程使用的工厂

  • RejectedExecutionHandler handler // 当任务无法执行时的处理器(线程拒绝策略)

    2. 核心类变量

    2.1 ctl变量

    ThreadPoolExecutor中有一个控制状态的属性叫ctl,它是一个AtomicInteger类型的变量,它一个int值可以储存两个概念的信息:

  • workerCount:表明当前池中有效的线程数,通过workerCountOf方法获得,workerCount上限是(2^29)-1。(最后存放在ctl的低29bit)

  • runState:表明当前线程池的状态,通过workerCountOf方法获得,最后存放在ctl的高3bit中,他们是整个线程池的运行生命周期,有如下取值,分别的含义是:

    1. RUNNING:可以新加线程,同时可以处理queue中的线程。线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,
    2. SHUTDOWN:不增加新线程,但是可以处理queue中的线程。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。
    3. STOP 不增加新线程,同时不处理queue中的线程,会中断正在处理任务的线程。调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
    4. TIDYING 当所有的任务已终止,ctl记录的”任务数量”为0,阻塞队列为空,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
    5. TERMINATED 线程池彻底终止,就变成TERMINATED状态。线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

COUNT_BITS=32(integer的size)-3=29,于是五种状态左移29位分别是:

  • RUNNING: 11100000000000000000000000000000
  • SHUTDOWN: 00000000000000000000000000000000
  • STOP: 00100000000000000000000000000000
  • TIDYING: 01000000000000000000000000000000
  • TERMINATED:01100000000000000000000000000000
    而ThreadPoolExecutor是通过runStateOf和workerCountOf获得者两个概念的值的。

用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。

runStateOf和workerCountOf方法是如何剥离出ctl变量的两个有效值呢?这其中我们可以看到CAPACITY是实现一个字段存两个值的最重要的字段。

2.2 CAPACITY变量

CAPACITY=(1 << COUNT_BITS) – 1 转成二进制为:000 11111111111111111111111111111,他是线程池理论上可以允许的最大的线程数。
所以很明显,它的重点在于,其高3bit为0,低29bit为1;
这样,workderCountOf方法中,CAPACITY和ctl进行&运算时,它能获得高3位都是0,低29位和ctl低29位相同的值,这个值就是workerCount;
同理,runStateOf方法,CAPACITY的取反和ctl进行&操作,获得高3位和ctl高三位相等,低29位都为0的值,这个值就是runState;

2.3 workQueue

1
2
3
4
5
6
7
8
9
10
11
12
/**
* The queue used for holding tasks and handing off to worker
* threads. We do not require that workQueue.poll() returning
* null necessarily means that workQueue.isEmpty(), so rely
* solely on isEmpty to see if the queue is empty (which we must
* do for example when deciding whether to transition from
* SHUTDOWN to TIDYING). This accommodates special-purpose
* queues such as DelayQueues for which poll() is allowed to
* return null even if it may later return non-null when delays
* expire.
*/
private final BlockingQueue<Runnable> workQueue;

一个BlockingQueue队列,本身的结构可以保证访问的线程安全(这里不展开了)。这是一个排队等待队列。当我们线程池里线程达到corePoolSize的时候,一些需要等待执行的线程就放在这个队列里等待。

2.4 workers

1
2
3
4
5
/**
* Set containing all worker threads in pool. Accessed only when
* holding mainLock.
*/
private final HashSet<Worker> workers = new HashSet<Worker>();

一个HashSet的集合。线程池里所有可以立即执行的线程都放在这个集合里。这也是我们直观理解的线程的池子。

2.5 mainLock

1
private final ReentrantLock mainLock = new ReentrantLock();

mainLock是线程池的主锁,是可重入锁,当要操作workers set这个保持线程的HashSet时,需要先获取mainLock,还有当要处理largestPoolSize、completedTaskCount这类统计数据时需要先获取mainLock

2.6 其他重要属性

1
2
3
4
5
private int largestPoolSize;   //用来记录线程池中曾经出现过的最大线程数

private long completedTaskCount; //用来记录已经执行完毕的任务个数

private volatile boolean allowCoreThreadTimeOut; //是否允许为核心线程设置存活时间

3 核心内部类

3.1 Worker

Worker类是线程池中具化一个线程的对象,线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker。我们来看看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* Class Worker mainly maintains interrupt control state for
* threads running tasks, along with other minor bookkeeping.
* This class opportunistically extends AbstractQueuedSynchronizer
* to simplify acquiring and releasing a lock surrounding each
* task execution. This protects against interrupts that are
* intended to wake up a worker thread waiting for a task from
* instead interrupting a task being run. We implement a simple
* non-reentrant mutual exclusion lock rather than use
* ReentrantLock because we do not want worker tasks to be able to
* reacquire the lock when they invoke pool control methods like
* setCorePoolSize. Additionally, to suppress interrupts until
* the thread actually starts running tasks, we initialize lock
* state to a negative value, and clear it upon start (in
* runWorker).
*/
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
//设置AQS的同步状态private volatile int state,是一个计数器,大于0代表锁已经被获取
// 在调用runWorker()前,禁止interrupt中断,在interruptIfStarted()方法中会判断 getState()>=0
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);//根据当前worker创建一个线程对象
//当前worker本身就是一个runnable任务,也就是不会用参数的firstTask创建线程,而是调用当前worker.run()时调用firstTask.run()
//后面在addworker中,我们会启动worker对象中组合的Thread,而我们的执行逻辑runWorker方法是在worker的run方法中被调用。
//为什么执行thread的run方法会调用worker的run方法呢,原因就是在这里进行了注入,将worker本身this注入到了thread中
}
/** Delegates main run loop to outer runWorker */
public void run() {
runWorker(this);
}//runWorker()是ThreadPoolExecutor的方法

// Lock methods
//
// The value 0 represents the unlocked state. 0代表“没被锁定”状态
// The value 1 represents the locked state. 1代表“锁定”状态
protected boolean isHeldExclusively() {
return getState() != 0;
}
/**
* 尝试获取锁
* 重写AQS的tryAcquire(),AQS本来就是让子类来实现的
*/
protected boolean tryAcquire(int unused) {
//尝试一次将state从0设置为1,即“锁定”状态,但由于每次都是state 0->1,而不是+1,那么说明不可重入
//且state==-1时也不会获取到锁
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 尝试释放锁
* 不是state-1,而是置为0
*/
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}

public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
/**
* 中断(如果运行)
* shutdownNow时会循环对worker线程执行
* 且不需要获取worker锁,即使在worker运行时也可以中断
*/
void interruptIfStarted() {
Thread t;
//如果state>=0、t!=null、且t没有被中断
//new Worker()时state==-1,说明不能中断
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}

Worker这个工作线程实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;

firstTask用来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。

之所以要将thread和firstTask封装成一个Worker,是因为线程池需要利用Worker来实现工作线程的加锁,以及控制中断。

Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。

  1. lock方法一旦获取了独占锁,表示当前线程正在执行任务中。
  2. 如果正在执行任务,则不应该中断线程。
  3. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
  4. 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。

4 核心方法

好了,基本上我们将线程池的几个主角,ctl,workQueue,workers,Worker简单介绍了一遍,现在,我们来看看线程池是怎么玩的。

4.1 线程的运行

我们先来看一个简单的线程池的运行流程图:

4.1.1 execute方法

这是线程池实现类外露供给外部实现提交线程任务command的核心方法,对于无需了解线程池内部的使用者来说,这个方法就是把某个任务交给线程池,正常情况下,这个任务会在未来某个时刻被执行,实现和注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Executes the given task sometime in the future. The task
* may execute in a new thread or in an existing pooled thread.
* * 在未来的某个时刻执行给定的任务。这个任务用一个新线程执行,或者用一个线程池中已经存在的线程执行
*
* If the task cannot be submitted for execution, either because this
* executor has been shutdown or because its capacity has been reached,
* the task is handled by the current {@code RejectedExecutionHandler}.
* 如果任务无法被提交执行,要么是因为这个Executor已经被shutdown关闭,要么是已经达到其容量上限,任务会被当前的RejectedExecutionHandler处理
*
* @param command the task to execute
* @throws RejectedExecutionException at discretion of
* {@code RejectedExecutionHandler}, if the task
* cannot be accepted for execution
* @throws NullPointerException if {@code command} is null
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
* 如果运行的线程少于corePoolSize,尝试开启一个新线程去运行command,command作为这个线程的第一个任务
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
* 如果任务成功放入队列,我们仍需要一个双重校验去确认是否应该新建一个线程(因为可能存在有些线程在我们上次检查后死了)
* 或者 从我们进入这个方法后,pool被关闭了
* 所以我们需要再次检查state,如果线程池停止了需要回滚入队列,如果池中没有线程了,新开启一个线程
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
* 如果无法将任务入队列(可能队列满了),需要新开区一个线程(自己:往maxPoolSize发展)
* 如果失败了,说明线程池shutdown 或者 饱和了,所以我们拒绝任务
*/
int c = ctl.get();
// 1、如果当前线程数少于corePoolSize(addWorker()操作已经包含对线程池状态的判断,所以此处没判断状态,而入workQueue前判断了)
if (workerCountOf(c) < corePoolSize) {
//则创建并启动一个线程来执行这个任务
//第一个参数为command,说明表示新建一个worker线程,指定firstTask初始任务为command
//第二个参数为true代表占用corePoolSize,false占用maxPoolSize
if (addWorker(command, true))
return;

/**
* 如果没有成功addWorker(),再次获取c(凡是需要再次用ctl做判断时,都会再次调用ctl.get())
* 失败的原因可能是:
* 1、线程池已经shutdown,shutdown的线程池不再接收新任务
* 2、workerCountOf(c) < corePoolSize 判断后,由于并发,别的线程先创建了worker线程,导致workerCount>=corePoolSize
*/
c = ctl.get();
}
/**
* 2、此时,workerCount >= corePoolSize,任务要加入队列,如果线程池是RUNNING状态,且入队列成功(阻塞队列未满)
*/
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();

/**
* 再次校验放入workerQueue中的任务是否能被执行。
* 如果线程池不是运行状态了,应该拒绝添加新任务,从workQueue中删除任务,执行拒绝策略
*/
//如果再次校验过程中,线程池不是RUNNING状态,那么任务出列,remove(command)--workQueue.remove()成功
if (! isRunning(recheck) && remove(command))
//执行拒绝策略
reject(command);

//走到这里,说明线程池是RUNNING状态。
//如果当前worker数量为0,通过addWorker(null, false)创建一个线程,其任务为null
//为什么只检查运行的worker数量是不是0呢?? 为什么不和corePoolSize比较呢??
//只保证有一个worker线程可以从queue中获取任务执行就行了??
//是的,因为只要还有活动的worker线程,就可以消费workerQueue中的任务
else if (workerCountOf(recheck) == 0)
//第一个参数为null,说明只为新建一个worker线程,没有指定firstTask初始任务
//第二个参数为true代表占用corePoolSize,false占用maxPoolSize
addWorker(null, false);
}
/**
* 3、如果线程池不是running状态 或者 无法入队列
* 尝试开启新线程,扩容至maxPoolSize,如果addWork(command, false)失败了,拒绝当前command
*/
else if (!addWorker(command, false))
reject(command);
}

我们可以简单归纳如下:

4.1.2 addWorker

在execute方法中,我们看到核心的逻辑是由addWorker方法来实现的,当我们将一个任务提交给线程池,线程池会如何处理,就是主要由这个方法加以规范:

该方法有两个参数:

  1. firstTask: worker线程的初始任务,可以为空
  2. core: true:将corePoolSize作为上限,false:将maximumPoolSize作为上限

排列组合,addWorker方法有4种传参的方式:

  1. addWorker(command, true)
  2. addWorker(command, false)
  3. addWorker(null, false)
  4. addWorker(null, true)

在execute方法中就使用了前3种,结合这个核心方法进行以下分析

  • 第一个:线程数小于corePoolSize时,放一个需要处理的task进Workers Set。如果Workers Set长度超过corePoolSize,就返回false
  • 第二个:当队列被放满时,就尝试将这个新来的task直接放入Workers Set,而此时Workers Set的长度限制是maximumPoolSize。如果线程池也满了的话就返回false
  • 第三个:放入一个空的task进workers Set,长度限制是maximumPoolSize。这样一个task为空的worker在线程执行的时候会去任务队列里拿任务,这样就相当于创建了一个新的线程,只是没有马上分配任务
  • 第四个:这个方法就是放一个空的task进Workers Set,而且是在小于corePoolSize时,如果此时Set中的数量已经达到corePoolSize那就返回false,什么也不干。实际使用中是在prestartAllCoreThreads()方法,这个方法用来为线程池预先启动corePoolSize个worker等待从workQueue中获取任务执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/**
* Checks if a new worker can be added with respect to current
* pool state and the given bound (either core or maximum). If so,
* the worker count is adjusted accordingly, and, if possible, a
* new worker is created and started, running firstTask as its
* first task. This method returns false if the pool is stopped or
* eligible to shut down. It also returns false if the thread
* factory fails to create a thread when asked. If the thread
* creation fails, either due to the thread factory returning
* null, or due to an exception (typically OutOfMemoryError in
* Thread.start()), we roll back cleanly.
* 检查根据当前线程池的状态和给定的边界(core or maximum)是否可以创建一个新的worker
* 如果是这样的话,worker的数量做相应的调整,如果可能的话,创建一个新的worker并启动,参数中的firstTask作为worker的第一个任务
* 如果方法返回false,可能因为pool已经关闭或者调用过了shutdown
* 如果线程工厂创建线程失败,也会失败,返回false
* 如果线程创建失败,要么是因为线程工厂返回null,要么是发生了OutOfMemoryError
*
* @param firstTask the task the new thread should run first (or
* null if none). Workers are created with an initial first task
* (in method execute()) to bypass queuing when there are fewer
* than corePoolSize threads (in which case we always start one),
* or when the queue is full (in which case we must bypass queue).
* Initially idle threads are usually created via
* prestartCoreThread or to replace other dying workers.
*
* @param core if true use corePoolSize as bound, else
* maximumPoolSize. (A boolean indicator is used here rather than a
* value to ensure reads of fresh values after checking other pool
* state).
* @return true if successful
*/
private boolean addWorker(Runnable firstTask, boolean core) {
//外层循环,负责判断线程池状态
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
/**
* 线程池的state越小越是运行状态,runnbale=-1,shutdown=0,stop=1,tidying=2,terminated=3
* 要想这个if为true,线程池state必须已经至少是shutdown状态了
* 这时候以下3个条件任意一个是false都会进入if语句,即无法addWorker():
* 1,rs == SHUTDOWN (隐含:rs>=SHUTDOWN)false情况: 线程池状态已经超过shutdown,
* 可能是stop、tidying、terminated其中一个,即线程池已经终止
* 2,firstTask == null (隐含:rs==SHUTDOWN)false情况: firstTask不为空,rs==SHUTDOWN 且 firstTask不为空,
* return false,场景是在线程池已经shutdown后,还要添加新的任务,拒绝
* 3,! workQueue.isEmpty() (隐含:rs==SHUTDOWN,firstTask==null)false情况: workQueue为空,
* 当firstTask为空时是为了创建一个没有任务的线程,再从workQueue中获取任务,
* 如果workQueue已经为空,那么就没有添加新worker线程的必要了
* return false,
*/
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
//内层循环,负责worker数量+1
for (;;) {
int wc = workerCountOf(c);
//入参core在这里起作用,表示加入的worker是加入corePool还是非corepool,换句话说,受到哪个size的约束
//如果worker数量>线程池最大上限CAPACITY(即使用int低29位可以容纳的最大值)
//或者( worker数量>corePoolSize 或 worker数量>maximumPoolSize ),即已经超过了给定的边界,不添加worker
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//CAS尝试增加线程数,,如果成功加了wc,那么break跳出检查
//如果失败,证明有竞争,那么重新到retry。
if (compareAndIncrementWorkerCount(c))
break retry;
//如果不成功,重新获取状态继续检查
c = ctl.get(); // Re-read ctl
//如果状态不等于之前获取的state,跳出内层循环,继续去外层循环判断
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
// else CAS失败时因为workerCount改变了,继续内层循环尝试CAS对worker数量+1
}
}
//worker数量+1成功的后续操作
// 添加到workers Set集合,并启动worker线程
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//新建worker//构造方法做了三件事//1、设置worker这个AQS锁的同步状态state=-1
w = new Worker(firstTask); //2、将firstTask设置给worker的成员变量firstTask
//3、使用worker自身这个runnable,调用ThreadFactory创建一个线程,并设置给worker的成员变量thread
final Thread t = w.thread;
if (t != null) {
//获取重入锁,并且锁上
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
// rs!=SHUTDOWN ||firstTask!=null
// 如果线程池在运行running<shutdown 或者
// 线程池已经shutdown,且firstTask==null(可能是workQueue中仍有未执行完成的任务,创建没有初始任务的worker线程执行)
// worker数量-1的操作在最后的addWorkerFailed()
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // // precheck that t is startable 线程已经启动,抛非法线程状态异常
throw new IllegalThreadStateException();
workers.add(w);
//设置最大的池大小largestPoolSize,workerAdded设置为true
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {//如果往HashSet中添加worker成功,启动线程
//通过t.start()方法正式执行线程。在这里一个线程才算是真正的执行起来了。
t.start();
workerStarted = true;
}
}
} finally {
//如果启动线程失败
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

同样的,我们可以归纳一下:

4.1.3 runWorker 方法

在addWorker方法中,我们将一个新增进去的worker所组合的线程属性thread启动了,但我们知道,在worker的构造方法中,它将自己本身注入到了thread的target属性里,所以绕了一圈,线程启动后,调用的还是worker的run方法,而在这里面,runWorker定义了线程执行的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Main worker run loop. Repeatedly gets tasks from queue and
* executes them, while coping with a number of issues:
*
* 1. We may start out with an initial task, in which case we
* don't need to get the first one. Otherwise, as long as pool is
* running, we get tasks from getTask. If it returns null then the
* worker exits due to changed pool state or configuration
* parameters. Other exits result from exception throws in
* external code, in which case completedAbruptly holds, which
* usually leads processWorkerExit to replace this thread.
* 我们可能使用一个初始化任务开始,即firstTask为null
* 然后只要线程池在运行,我们就从getTask()获取任务
* 如果getTask()返回null,则worker由于改变了线程池状态或参数配置而退出
* 其它退出因为外部代码抛异常了,这会使得completedAbruptly为true,这会导致在processWorkerExit()方法中替换当前线程
*
* 2. Before running any task, the lock is acquired to prevent
* other pool interrupts while the task is executing, and then we
* ensure that unless pool is stopping, this thread does not have
* its interrupt set.
* 在任何任务执行之前,都需要对worker加锁去防止在任务运行时,其它的线程池中断操作
* clearInterruptsForTaskRun保证除非线程池正在stoping,线程不会被设置中断标示
*
* 3. Each task run is preceded by a call to beforeExecute, which
* might throw an exception, in which case we cause thread to die
* (breaking loop with completedAbruptly true) without processing
* the task.
* 每个任务执行前会调用beforeExecute(),其中可能抛出一个异常,这种情况下会导致线程die(跳出循环,且completedAbruptly==true),没有执行任务
* 因为beforeExecute()的异常没有cache住,会上抛,跳出循环
*
* 4. Assuming beforeExecute completes normally, we run the task,
* gathering any of its thrown exceptions to send to afterExecute.
* We separately handle RuntimeException, Error (both of which the
* specs guarantee that we trap) and arbitrary Throwables.
* Because we cannot rethrow Throwables within Runnable.run, we
* wrap them within Errors on the way out (to the thread's
* UncaughtExceptionHandler). Any thrown exception also
* conservatively causes thread to die.
*
* 5. After task.run completes, we call afterExecute, which may
* also throw an exception, which will also cause thread to
* die. According to JLS Sec 14.20, this exception is the one that
* will be in effect even if task.run throws.
*
* The net effect of the exception mechanics is that afterExecute
* and the thread's UncaughtExceptionHandler have as accurate
* information as we can provide about any problems encountered by
* user code.
*
* @param w the worker
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
//标识线程是不是异常终止的
boolean completedAbruptly = true;
try {
//task不为null的情况一般是线程数小于核心数时,如果task为null,则去队列中取任务--->getTask()
//可以看到,只要getTask方法被调用且返回null,那么worker必定被销毁,因为一旦while进不去,就会执行processWorkerExit方法,销毁线程。

//而确定一个线程能否获取得到task的逻辑,在getTask方法中
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//线程开始执行之前执行此方法,可以实现Worker未执行退出,本类中未实现
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();//runWorker方法最本质的存在意义,就是调用task的run方法
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
//线程执行后执行,可以实现标识Worker异常中断的功能,本类中未实现
afterExecute(task, thrown);
}
} finally {
task = null;//运行过的task标null
w.completedTasks++;
w.unlock();
}
}
//标识线程不是异常终止的,是因为不满足while条件,被迫销毁的
completedAbruptly = false;
} finally {
//处理worker退出的逻辑
processWorkerExit(w, completedAbruptly);
}
}

我们归纳:

4.1.4 getTask方法

runWorker方法中的getTask()方法是线程处理完一个任务后,从队列中获取新任务的实现,也是处理判断一个线程是否应该被销毁的逻辑所在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* Performs blocking or timed wait for a task, depending on
* current configuration settings, or returns null if this worker
* must exit because of any of: 以下情况会返回null
* 1. There are more than maximumPoolSize workers (due to
* a call to setMaximumPoolSize).
* 超过了maximumPoolSize设置的线程数量(因为调用了setMaximumPoolSize())
* 2. The pool is stopped.
* 线程池被stop
* 3. The pool is shutdown and the queue is empty.
* 线程池被shutdown,并且workQueue空了
* 4. This worker timed out waiting for a task, and timed-out
* workers are subject to termination (that is,
* {@code allowCoreThreadTimeOut || workerCount > corePoolSize})
* both before and after the timed wait.
* 线程等待任务超时
*
* @return task, or null if the worker must exit, in which case
* workerCount is decremented
* 返回null表示这个worker要结束了,这种情况下workerCount-1
*/
private Runnable getTask() {
// timedOut 主要是判断后面的poll是否要超时
boolean timedOut = false; // Did the last poll() time out?

/**
* 用于判断线程池状态
*/
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
/**
* 对线程池状态的判断,两种情况会workerCount-1,并且返回null
* 1,线程池状态为shutdown,且workQueue为空(反映了shutdown状态的线程池还是要执行workQueue中剩余的任务的)
* 2,线程池状态为>=stop(只有TIDYING和TERMINATED会大于stop)(shutdownNow()会导致变成STOP)(此时不用考虑workQueue的情况)
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();//循环的CAS减少worker数量,直到成功
return null;
}

int wc = workerCountOf(c);

// Are workers subject to culling?

//allowCoreThreadTimeOut字段,表示是否允许核心线程超过闲置时间后被摧毁,默认为false
//我们前面说过,如果getTask方法返回null,那么这个worker只有被销毁一途
//于是这个timed有3种情况
//(1)当线程数没有超过核心线程数,且默认allowCoreThreadTimeOut为false时
// timed值为false。看下面if的判断逻辑,除非目前线程数大于最大值,否则下面的if始终进不去,该方法不可能返回null,worker也就不会被销毁。
// 因为前提"线程数不超过核心线程数"与"线程数大于最大值"两个命题互斥,所以(1)情况,逻辑进入下面的if(返回null的线程销毁逻辑)的可能性不存在。
// 也就是说,当线程数没有超过核心线程数时,线程不会被销毁。
//(2)当当前线程数超过核心线程数,且默认allowCoreThreadTimeOut为false时
// timed值为true。
//(3)如果allowCoreThreadTimeOut为true,则timed始终为true
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

//wc > maximumPoolSize则必销毁,因为这情况下,wc>1也肯定为true
//wc <= maximumPoolSize,且(timed && timedOut) = true,这种情况下一般也意味着worker要被销毁,因为超时一般是由阻塞队列为空造成的,所以workQueue.isEmpty()也大概率为真,进入if逻辑。

//一般情况是这样,那不一般的情况呢?阻塞队列没有为空,但是因为一些原因,还是超时了,这时候取决于wc > 1,它为真就销毁,为假就不销毁。
// 也就是说,如果阻塞队列还有任务,但是wc=1,线程池里只剩下自己这个线程了,那么就不能销毁,这个if不满足,我们的代码继续往下走
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}

try {
//如果timed为true那么使用poll取线程。否则使用take()
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
//workQueue.poll():如果在keepAliveTime时间内,阻塞队列还是没有任务,返回null
workQueue.take();
//workQueue.take():如果阻塞队列为空,当前线程会被挂起等待;当队列中有任务加入时,线程被唤醒,take方法返回任务
//如果正常返回,那么返回取到的task。
if (r != null)
return r;
//否则,设为超时,重新执行循环,
timedOut = true;
} catch (InterruptedException retry) {
//当线程阻塞在从workQueue中获取任务时,可以被interrupt()中断,代码中捕获了InterruptedException,重置timedOut为初始值false,再次执行第1步中的判断,满足就继续获取任务,不满足return null,会进入worker退出的流程
timedOut = false;
}
}

归纳:

4.1.5 processWorkerExit方法

在runWorker方法中,我们看到当不满足while条件后,线程池会执行退出线程的操作,这个操作,就封装在processWorkerExit方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* Performs cleanup and bookkeeping for a dying worker. Called
* only from worker threads. Unless completedAbruptly is set,
* assumes that workerCount has already been adjusted to account
* for exit. This method removes thread from worker set, and
* possibly terminates the pool or replaces the worker if either
* it exited due to user task exception or if fewer than
* corePoolSize workers are running or queue is non-empty but
* there are no workers.
*
* @param w the worker
* @param completedAbruptly if the worker died due to user exception
*/
private void processWorkerExit(Worker w, boolean completedAbruptly) {
//参数:
//worker: 要结束的worker
//completedAbruptly: 是否突然完成(是否因为异常退出)

/**
* 1、worker数量-1
* 如果是突然终止,说明是task执行时异常情况导致,即run()方法执行时发生了异常,那么正在工作的worker线程数量需要-1
* 如果不是突然终止,说明是worker线程没有task可执行了,不用-1,因为已经在getTask()方法中-1了
*/
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted 代码和注释正好相反啊
decrementWorkerCount();

/**
* 2、从Workers Set中移除worker
*/
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks; //把worker的完成任务数加到线程池的完成任务数
workers.remove(w); //从HashSet<Worker>中移除
} finally {
mainLock.unlock();
}

/**
* 3、在对线程池有负效益的操作时,都需要“尝试终止”线程池
* 主要是判断线程池是否满足终止的状态
* 如果状态满足,但线程池还有线程,尝试对其发出中断响应,使其能进入退出流程
* 没有线程了,更新状态为tidying->terminated
*/
tryTerminate();

/**
* 4、是否需要增加worker线程
* 线程池状态是running 或 shutdown
* 如果当前线程是突然终止的,addWorker()
* 如果当前线程不是突然终止的,但当前线程数量 < 要维护的线程数量,addWorker()
* 故如果调用线程池shutdown(),直到workQueue为空前,线程池都会维持corePoolSize个线程,然后再逐渐销毁这corePoolSize个线程
*/
int c = ctl.get();
//如果状态是running、shutdown,即tryTerminate()没有成功终止线程池,尝试再添加一个worker
if (runStateLessThan(c, STOP)) {
//不是突然完成的,即没有task任务可以获取而完成的,计算min,并根据当前worker数量判断是否需要addWorker()
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize; //allowCoreThreadTimeOut默认为false,即min默认为corePoolSize

//如果min为0,即不需要维持核心线程数量,且workQueue不为空,至少保持一个线程
if (min == 0 && ! workQueue.isEmpty())
min = 1;

//如果线程数量大于最少数量,直接返回,否则下面至少要addWorker一个
if (workerCountOf(c) >= min)
return; // replacement not needed
}

//添加一个没有firstTask的worker
//只要worker是completedAbruptly突然终止的,或者线程数量小于要维护的数量,就新添一个worker线程,即使是shutdown状态
addWorker(null, false);
}
}

总而言之:如果线程池还没有完全终止,就仍需要保持一定数量的线程。

线程池状态是running 或 shutdown的情况下:

  • 如果当前线程是突然终止的,addWorker()
  • 如果当前线程不是突然终止的,但当前线程数量 < 要维护的线程数量,addWorker()

故如果调用线程池shutdown(),直到workQueue为空前,线程池都会维持corePoolSize个线程,然后再逐渐销毁这corePoolSize个线程。


4.1.6 submit方法

前面我们讲过execute方法,其作用是将一个任务提交给线程池,以期在未来的某个时间点被执行。

submit方法在作用上,和execute方法是一样的,将某个任务提交给线程池,让线程池调度线程去执行它。

那么它和execute方法有什么区别呢?我们来看看submit方法的源码:
submit方法的实现在ThreadPoolExecutor的父类AbstractExecutorService类中,有三种重载方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    /**
* 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该Future的get方法在成功完成时将会返回null。
* submit 参数: task - 要提交的任务 返回:表示任务等待完成的 Future
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}

/**
* 提交一个Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功完成时将会返回给定的结果。
* submit 参数: task - 要提交的任务 result - 完成任务时要求返回的结果
* 返回: 表示任务等待完成的 Future
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}

/**
* 提交一个Callable的任务用于执行,返回一个表示任务的未决结果的 Future。该 Future 的 get
方法在成功完成时将会返回该任务的结果。
* 如果想立即阻塞任务的等待,则可以使用 result =
exec.submit(aCallable).get(); 形式的构造。
* 参数: task - 要提交的任务 返回: 表示任务等待完成的Future
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}

源码很简单,submit方法,将任务task封装成FutureTask(newTaskFor方法中就是new了一个FutureTask),然后调用execute。所以submit方法和execute的所有区别,都在这FutureTask所带来的差异化实现上。

总而言之,submit方法将一个任务task用future模式封装成FutureTask对象,提交给线程执行,并将这个FutureTask对象返回,以供主线程在该任务被线程池执行之后得到执行结果。

注意,获得执行结果的方法FutureTask.get(),会阻塞执行该方法的线程,尤其是当任务被DiscardPolicy策略和DiscardOldestPolicy拒绝的时候,get方法会一直阻塞在那里,所以我们最好使用自带超时时间的future。

4.2 线程池的关闭

4.2.1 shutdown方法

讲完了线程池的基本运转过程,在方法章的最后,我们来看看负责线程池生命周期最后收尾工作的几个重要方法,首先是shutdown方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Initiates an orderly shutdown in which previously submitted
* tasks are executed, but no new tasks will be accepted.
* Invocation has no additional effect if already shut down.
*
* <p>This method does not wait for previously submitted tasks to
* complete execution. Use {@link #awaitTermination awaitTermination}
* to do that.
* 开始一个顺序的shutdown操作,shutdown之前被执行的已提交任务,新的任务不会再被接收了。如果线程池已经被shutdown了,该方法的调用没有其他任何效果了。
* **该方法不会等待之前已经提交的任务执行完毕**,awaitTermination方法才有这个效果。
*
* @throws SecurityException {@inheritDoc}
*/
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//判断是否可以操作关闭目标线程。
checkShutdownAccess();
//advanceRunState方法,参数:目标状态;作用:一直执行,直到成功利用CAS将状态置为目标值。
//设置线程池状态为SHUTDOWN,此处之后,线程池中不会增加新Task
advanceRunState(SHUTDOWN);
//中断所有的空闲线程
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
//尝试进行terminate操作,但其实我们上面将状态置为shutdown,就已经算是“中止”了一个线程池了,它不会再执行任务,于外部而言,已经失去了作用。而这里,也只是尝试去将线程池的状态一撸到底而已,并不是一定要terminate掉。该方法我们后面会说到。
tryTerminate();
}

我们可以看到,shutdown方法只不过是中断唤醒了所有阻塞的线程,并且把线程池状态置为shutdown,正如注释所说的,它没有等待所有正在执行任务的线程执行完任务,把状态置为shutdown,已经足够线程池丧失基本的功能了。

在该方法中,线程池如何中断线程是我们最需要关心的,我们来看一下interruptIdleWorkers方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void interruptIdleWorkers(boolean onlyOne) {//参数onlyOne表示是否只中断一个线程就退出,在shutdown中该值为false。
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//遍历workers 对所有worker做中断处理。
for (Worker w : workers) {
Thread t = w.thread;
// w.tryLock()对Worker获取锁,因为正在执行的worker已经加锁了(见runWorker方法,w.lock()语句)
//所以这保证了正在运行执行Task的Worker不会被中断。只有阻塞在getTask方法的空闲线程才会进这个if判断(被中断),但中断不代表线程立刻停止,它要继续处理到阻塞队列为空时才会被销毁。
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}

我们可以看到,在中断方法中,我们调用了worker的tryLock方法去尝试获取worker的锁,所以我们说,worker类这一层的封装,是用来控制线程中断的,正在执行任务的线程已经上了锁,无法被中断,只有在获取阻塞队列中的任务的线程(我们称为空闲线程)才会有被中断的可能。

之前我们看过getTask方法,在这个方法中, worker是不加锁的,所以可以被中断。我们为什么说“中断不代表线程立刻停止,它要继续处理到阻塞队列为空时才会被销毁”呢?具体逻辑,我们再来看一下getTask的源码,以及我们的注释(我们模拟中断发生时的场景):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?

/**
* 当执行过程中抛出InterruptedException 的时候,该异常被catch住,逻辑重新回到这个for循环
* catch块在getTask方法的最后。
*/
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
/**
* 因为逻辑是在抛出中断异常后来到这里的,那说明线程池的状态已经在shutdown方法中被置为shutdown了,rs >= SHUTDOWN为true,rs >=STOP为false(只有TIDYING和TERMINATED状态会大于stop)
* 这时候,如果workQueue为空,判断为真,线程被销毁。
* 否则,workQueue为非空,判断为假,线程不会进入销毁逻辑。
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();//循环的CAS减少worker数量,直到成功
return null;
}

int wc = workerCountOf(c);

// Are workers subject to culling?

//因为在catch块中,timeOut已经为false了。
//所以只要不发生当前线程数超过最大线程数这种极端情况,命题(wc > maximumPoolSize || (timed && timedOut)一定为false,线程依旧不被销毁。
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}

try {
//继续执行正常的从阻塞队列中取任务的逻辑,直到阻塞队列彻底为空,这时候,上面第一个if判断符合,线程被销毁,寿命彻底结束。
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
//如果正常返回,那么返回取到的task。
if (r != null)
return r;
//否则,设为超时,重新执行循环,
timedOut = true;
} catch (InterruptedException retry) {
//捕获中断异常
timedOut = false;
}
}
}

总结:正阻塞在getTask()获取任务的worker在被中断后,会抛出InterruptedException,不再阻塞获取任务。捕获中断异常后,将继续循环到getTask()最开始的判断线程池状态的逻辑,当线程池是shutdown状态,且workQueue.isEmpty时,return null,进行worker线程退出逻辑。

所以,这就是我们为什么说,shutdown方法不会立刻停止线程池,它的作用是阻止新的任务被添加进来(逻辑在addWorker方法的第一个if判断中,可以返回去看一下),并且继续处理完剩下的任务,然后tryTerminated,尝试关闭。

4.2.2 tryTerminate方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* Transitions to TERMINATED state if either (SHUTDOWN and pool
* and queue empty) or (STOP and pool empty). If otherwise
* eligible to terminate but workerCount is nonzero, interrupts an
* idle worker to ensure that shutdown signals propagate. This
* method must be called following any action that might make
* termination possible -- reducing worker count or removing tasks
* from the queue during shutdown. The method is non-private to
* allow access from ScheduledThreadPoolExecutor.
* 在以下情况将线程池变为TERMINATED终止状态
* shutdown 且 正在运行的worker 和 workQueue队列 都empty
* stop 且 没有正在运行的worker
*
* 这个方法必须在任何可能导致线程池终止的情况下被调用,如:
* 减少worker数量
* shutdown时从queue中移除任务
*
* 这个方法不是私有的,所以允许子类ScheduledThreadPoolExecutor调用
*/
final void tryTerminate() {
for (;;) {
int c = ctl.get();
/**
* 线程池是否需要终止
* 如果以下3中情况任一为true,return,不进行终止
* 1、还在运行状态
* 2、状态是TIDYING、或 TERMINATED,已经终止过了
* 3、SHUTDOWN 且 workQueue不为空
*/
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
/**
* 只有shutdown状态 且 workQueue为空,或者 stop状态能执行到这一步
* 如果此时线程池还有线程(正在运行任务或正在等待任务,总之count不等于0)
* 中断唤醒一个正在等任务的空闲worker
*(中断唤醒的意思就是让阻塞在阻塞队列中的worker抛出异常,然后重新判断状态,getTask方法逻辑)
* 线程被唤醒后再次判断线程池状态,会return null,进入processWorkerExit()流程(runWorker逻辑)
*/
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);//中断workers集合中的空闲任务,参数为true,只中断一个。(该逻辑的意义应该在于通知被阻塞在队列中的线程:别瞎jb等了,这个线程池都要倒闭了,赶紧收拾铺盖准备销毁吧你个逼玩意儿)。
//尝试终止失败,返回。可能大家会有疑问,shutdown只调用了一次tryTerminate方法,如果一次尝试失败了,是不是就意味着shutdown方法很可能最终无法终止线程池?
//其实看注释,我们知道线程池在进行所有负面效益的操作时都会调用该方法尝试终止,上面我们中断了一个阻塞线程让他被销毁,他销毁时也会尝试终止(这其中又唤醒了一个阻塞线程去销毁),以此类推,直到最后一个线程执行tryTerminate时,逻辑才有可能走到下面去。
return;
}
/**
* 如果状态是SHUTDOWN,workQueue也为空了,正在运行的worker也没有了,开始terminated
*/
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//CAS:将线程池的ctl变成TIDYING(所有的任务被终止,workCount为0,为此状态时将会调用terminated()方法),期间ctl有变化就会失败,会再次for循环
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
//方法为空,需子类实现
terminated();
} finally {
//将状态置为TERMINATED
ctl.set(ctlOf(TERMINATED, 0));
//最后执行termination.signalAll(),并唤醒所有等待线程池终止这个Condition的线程(也就是调用了awaitTermination方法的线程,这个方法的作用是阻塞调用它的线程,直到调用该方法的线程池真的已经被终止了。)
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}

总结一下:tryTerminate被调用的时机主要有:

  1. shutdown方法时
  2. processWorkerExit方法销毁一个线程时
  3. addWorkerFailed方法添加线程失败或启动线程失败时
  4. remove方法,从阻塞队列中删掉一个任务时

4.2.3 shutdownNow方法

我们知道,shutdown后线程池将变成shutdown状态,此时不接收新任务,但会处理完正在运行的 和 在阻塞队列中等待处理的任务。

我们接下来要说的shutdownNow方法,作用是:shutdownNow后线程池将变成stop状态,此时不接收新任务,不再处理在阻塞队列中等待的任务,还会尝试中断正在处理中的工作线程。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Attempts to stop all actively executing tasks, halts the
* processing of waiting tasks, and returns a list of the tasks
* that were awaiting execution. These tasks are drained (removed)
* from the task queue upon return from this method.
* 尝试停止所有活动的正在执行的任务,停止等待任务的处理,并返回正在等待被执行的任务列表
* 这个任务列表是从任务队列中排出(删除)的
* <p>This method does not wait for actively executing tasks to
* terminate. Use {@link #awaitTermination awaitTermination} to
* do that.
* 这个方法不用等到正在执行的任务结束,要等待线程池终止可使用awaitTermination()
* <p>There are no guarantees beyond best-effort attempts to stop
* processing actively executing tasks. This implementation
* cancels tasks via {@link Thread#interrupt}, so any task that
* fails to respond to interrupts may never terminate.
* 除了尽力尝试停止运行中的任务,没有任何保证
* 取消任务是通过Thread.interrupt()实现的,所以任何响应中断失败的任务可能永远不会结束
* @throws SecurityException {@inheritDoc}
*/
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//判断调用者是否有权限shutdown线程池
checkShutdownAccess();
//CAS+循环设置线程池状态为stop
advanceRunState(STOP);
//中断所有线程,包括正在运行任务的
interruptWorkers();
//将workQueue中的元素放入一个List并返回
tasks = drainQueue();
} finally {
mainLock.unlock();
}
//尝试终止线程池
tryTerminate();
//返回workQueue中未执行的任务
return tasks;
}

interruptWorkers 很简单,循环对所有worker调用 interruptIfStarted,其中会判断worker的AQS state是否大于0,即worker是否已经开始运作,再调用Thread.interrupt

需要注意的是,对于运行中的线程调用Thread.interrupt并不能保证线程被终止,task.run内部可能捕获了InterruptException,没有上抛,导致线程一直无法结束

4.2.4 awaitTermination方法

该方法的作用是等待线程池终止,参数是timeout:超时时间和unit: timeout超时时间的单位,返回结果:true:线程池终止,false:超过timeout指定时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (;;) {
//是否terminated终止
if (runStateAtLeast(ctl.get(), TERMINATED))
return true;
//是否已经超过超时时间
if (nanos <= 0)
return false;
//核心逻辑:看注释我们能知道,该方法让调用线程等待一段时间,直到被唤醒(有且仅有之前我们说过的tryTerminate方法中的 termination.signalAll()),或者被异常中断,或者传入了nanos时间参数流逝完。
nanos = termination.awaitNanos(nanos);
}
} finally {
mainLock.unlock();
}
}

termination.awaitNanos() 是通过 LockSupport.parkNanos(this, nanosTimeout)实现的阻塞等待

阻塞等待过程中发生以下具体情况会解除阻塞(对上面3种情况的解释):

  1. 如果发生了 termination.signalAll()(内部实现是 LockSupport.unpark())会唤醒阻塞等待,且由于ThreadPoolExecutor只有在 tryTerminated()尝试终止线程池成功,将线程池更新为terminated状态后才会signalAll(),故awaitTermination()再次判断状态会return true退出

  2. 如果达到了超时时间 termination.awaitNanos() 也会返回,此时nano==0,再次循环判断return false,等待线程池终止失败

  3. 如果当前线程被 Thread.interrupt(),termination.awaitNanos()会上抛InterruptException,awaitTermination()继续上抛给调用线程,会以异常的形式解除阻塞

综上,要想优雅的关闭线程池,我们应该:

1
2
3
4
5
6
7
8
9
executorService.shutdown();
try{
while(!executorService.awaitTermination(500, TimeUnit.MILLISECONDS)) {
LOGGER.debug("Waiting for terminate");
}
}
catch (InterruptedException e) {
//中断处理
}

5 拒绝策略

我们最后来看一下线程池构造函数的最后一个参数:RejectedExecutionHandler。

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

拒绝策略是一个接口,其设计如下:

1
2
3
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

用户可以通过实现这个接口去定制拒绝策略,也可以选择JDK提供的四种已有拒绝策略,其特点如下:

四个拒绝策略的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* A handler for rejected tasks that runs the rejected task
* directly in the calling thread of the {@code execute} method,
* unless the executor has been shut down, in which case the task
* is discarded.
*/
public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code CallerRunsPolicy}.
*/
public CallerRunsPolicy() { }

/**
* Executes task r in the caller's thread, unless the executor
* has been shut down, in which case the task is discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}

/**
* A handler for rejected tasks that throws a
* {@code RejectedExecutionException}.
*/
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() { }

/**
* Always throws RejectedExecutionException.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
* @throws RejectedExecutionException always
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}

/**
* A handler for rejected tasks that silently discards the
* rejected task.
*/
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
public DiscardPolicy() { }

/**
* Does nothing, which has the effect of discarding task r.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}


/**
* A handler for rejected tasks that discards the oldest unhandled
* request and then retries {@code execute}, unless the executor
* is shut down, in which case the task is discarded.
*/
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardOldestPolicy} for the given executor.
*/
public DiscardOldestPolicy() { }

/**
* Obtains and ignores the next task that the executor
* would otherwise execute, if one is immediately available,
* and then retries execution of task r, unless the executor
* is shut down, in which case task r is instead discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}

核心的rejectedExecution方法,在ThreadPoolExecutor中被reject方法调用:

1
2
3
4
5
6
7
8

/**
* Invokes the rejected execution handler for the given command.
* Package-protected for use by ScheduledThreadPoolExecutor.
*/
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}

而reject方法在execute方法中被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 /**
* Executes the given task sometime in the future. The task
* may execute in a new thread or in an existing pooled thread.
*
* If the task cannot be submitted for execution, either because this
* executor has been shutdown or because its capacity has been reached,
* the task is handled by the current {@code RejectedExecutionHandler}.
*
* @param command the task to execute
* @throws RejectedExecutionException at discretion of
* {@code RejectedExecutionHandler}, if the task
* cannot be accepted for execution
* @throws NullPointerException if {@code command} is null
*/
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();

int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 如果线程池已经关闭,并且当前任务成功从队列中移除
if (! isRunning(recheck) && remove(command))
// 执行拒绝策略
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
// 如果添加任务至队列中失败,执行拒绝策略
reject(command);
}
1…67
cherish-ls

cherish-ls

纸上得来终觉浅

68 posts
27 categories
92 tags
GitHub
© 2021 cherish-ls | Site words total count: 457.5k
Powered by Hexo
|
Theme — NexT.Muse v5.1.4
访问人数 访问总量 次
0%