nginx deny all

仅允许某个网段访问,其他全部拒绝

1
2
3
4
5
6
server {
[…]
allow 192.168.0.0/16;
deny all;
error_page 403 /error403.html;
}

尽管允许某个网段访问,依然无法访问,原因是/error403.html页面也被deny了,正确的如下

1
2
3
4
5
6
7
8
9
10
11
server {
[…]
location / {
error_page 403 /error403.html;
allow 192.168.0.0/16;
deny all;
}
location = /error403.html {
allow all;
}
}

如果想配置多个错误页

1
2
3
4
5
6
7
8
9
10
11
12
server {
[…]
location / {
error_page 403 /error403.html;
error_page 404 /error404.html;
allow 192.168.0.0/16;
deny all;
}
location ~ "^/error[0-9]{3}\.html$" {
allow all;
}
}

nginx 400 bad request 客户端发送cookies过大

调整large_client_header_buffers大小

多条件rewrite

nginx 只有if指令没有else指令, 如果存在多重条件,可以通过多个变量实现

安装

http://zeromq.org/bindings:php
需要libzmq-dev包

简单模式

server

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
<?php
/*
* Hello World server
* Binds REP socket to tcp://*:5555
* Expects "Hello" from client, replies with "World"
* @author Ian Barber <ian(dot)barber(at)gmail(dot)com>
*/
$context = new ZMQContext(1);
// Socket to talk to clients
$responder = new ZMQSocket($context, ZMQ::SOCKET_REP);
$responder->bind("tcp://*:5555");
while (true) {
// Wait for next request from client
$request = $responder->recv();
printf ("Received request: [%s]\n", $request);
// Do some 'work'
sleep (1);
// Send reply back to client
$responder->send("World");
}

client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
/*
* Hello World client
* Connects REQ socket to tcp://localhost:5555
* Sends "Hello" to server, expects "World" back
* @author Ian Barber <ian(dot)barber(at)gmail(dot)com>
*/
$context = new ZMQContext();
// Socket to talk to server
echo "Connecting to hello world server…\n";
$requester = new ZMQSocket($context, ZMQ::SOCKET_REQ);
$requester->connect("tcp://localhost:5555");
for ($request_nbr = 0; $request_nbr != 10; $request_nbr++) {
printf ("Sending request %d…\n", $request_nbr);
$requester->send("Hello");
$reply = $requester->recv();
printf ("Received reply %d: [%s]\n", $request_nbr, $reply);
}

获取0mq版本号

1
2
3
4
5
6
7
8
9
<?php
/* Report 0MQ version
*
* @author Ian Barber <ian(dot)barber(at)gmail(dot)com>
*/
if (class_exists("ZMQ") && defined("ZMQ::LIBZMQ_VER")) {
echo ZMQ::LIBZMQ_VER, PHP_EOL;
}

发布-订阅模式

server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
/*
* Weather update server
* Binds PUB socket to tcp://*:5556
* Publishes random weather updates
* @author Ian Barber <ian(dot)barber(at)gmail(dot)com>
*/
// Prepare our context and publisher
$context = new ZMQContext();
$publisher = $context->getSocket(ZMQ::SOCKET_PUB);
$publisher->bind("tcp://*:5556");
$publisher->bind("ipc://weather.ipc");
while (true) {
// Get values that will fool the boss
$zipcode = mt_rand(0, 100000);
$temperature = mt_rand(-80, 135);
$relhumidity = mt_rand(10, 60);
// Send message to all subscribers
$update = sprintf ("%05d %d %d", $zipcode, $temperature, $relhumidity);
$publisher->send($update);
}

client

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
<?php
/*
* Weather update client
* Connects SUB socket to tcp://localhost:5556
* Collects weather updates and finds avg temp in zipcode
* @author Ian Barber <ian(dot)barber(at)gmail(dot)com>
*/
$context = new ZMQContext();
// Socket to talk to server
echo "Collecting updates from weather server…", PHP_EOL;
$subscriber = new ZMQSocket($context, ZMQ::SOCKET_SUB);
$subscriber->connect("tcp://localhost:5556");
// Subscribe to zipcode, default is NYC, 10001
$filter = $_SERVER['argc'] > 1 ? $_SERVER['argv'][1] : "10001";
$subscriber->setSockOpt(ZMQ::SOCKOPT_SUBSCRIBE, $filter);
// Process 100 updates
$total_temp = 0;
for ($update_nbr = 0; $update_nbr < 100; $update_nbr++) {
$string = $subscriber->recv();
sscanf ($string, "%d %d %d", $zipcode, $temperature, $relhumidity);
$total_temp += $temperature;
}
printf ("Average temperature for zipcode '%s' was %dF\n",
$filter, (int) ($total_temp / $update_nbr));

总结

速度最快的消息中间件,支持各种客户端,缺点是不能保证消息丢失

名词解释

从AMQP协议可以看出,MessageQueue、Exchange和Binding构成了AMQP协议的核心,下面我们就围绕这三个主要组件 从应用使用的角度全面的介绍如何利用Rabbit MQ构建消息队列以及使用过程中的注意事项。

MessageQueue

在Rabbit MQ中,无论是生产者发送消息还是消费者接受消息,都首先需要声明一个MessageQueue。这就存在一个问题,是生产者声明还是消费者声明呢?要解决这个问题,首先需要明确:

a)消费者是无法订阅或者获取不存在的MessageQueue中信息。

b)消息被Exchange接受以后,如果没有匹配的Queue,则会被丢弃。
在明白了上述两点以后,就容易理解如果是消费者去声明Queue,就有可能会出现在声明Queue之 前,生产者已发送的消息被丢弃的隐患。如果应用能够通过消息重发的机制允许消息丢失,则使用此方案没有任何问题。但是如果不能接受该方案,这就需要无论是 生产者还是消费者,在发送或者接受消息前,都需要去尝试建立消息队列。这里有一点需要明确,如果客户端尝试建立一个已经存在的消息队列,Rabbit MQ不会做任何事情,并返回客户端建立成功的。

如果一个消费者在一个信道中正在监听某一个队列的消息,Rabbit MQ是不允许该消费者在同一个channel去声明其他队列的。Rabbit MQ中,可以通过queue.declare命令声明一个队列,可以设置该队列以下属性:

a) Exclusive: 排他队列,如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意三点:其一,排他队列是基于连接可见 的,同一连接的不同信道是可以同时访问同一个连接创建的排他队列的。其二,“首次”,如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排 他队列的,这个与普通队列不同。其三,即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除的。这种队列适用于只限于一个客户端 发送读取消息的应用场景。

b) Auto-delete:自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。这种队列适用于临时队列。

c) Durable:持久化,这个会在后面作为专门一个章节讨论。

d) 其他选项,例如如果用户仅仅想查询某一个队列是否已存在,如果不存在,不想建立该队列,仍然可以调用queue.declare,只不过需要将参数passive设为true,传给queue.declare,如果该队列已存在,则会返回true;如果不存在,则会返回Error,但是不会创建新的队列。

生产者发送消息

在AMQP模型中,Exchange是接受生产者消息并将消息路由到消息队列的关键组件。ExchangeType和Binding决定了消息的路由规 则。所以生产者想要发送消息,首先必须要声明一个Exchange和该Exchange对应的Binding。可以通过 ExchangeDeclare和BindingDeclare完成。在Rabbit MQ中,声明一个Exchange需要三个参数:ExchangeName,ExchangeType和Durable。ExchangeName是该 Exchange的名字,该属性在创建Binding和生产者通过publish推送消息时需要指定。ExchangeType,指Exchange的类 型,在RabbitMQ中,有三种类型的Exchange:direct ,fanout和topic,不同的Exchange会表现出不同路由行为。Durable是该Exchange的持久化属性,这个会在消息持久化章节讨 论。声明一个Binding需要提供一个QueueName,ExchangeName和BindingKey。下面我们就分析一下不同的 ExchangeType表现出的不同路由规则。

生产者在发送消息时,都需要指定一个RoutingKey和Exchange,Exchange在接到该RoutingKey以后,会判断该ExchangeType:

a) 如果是Direct类型,则会将消息中的RoutingKey与该Exchange关联的所有Binding中的BindingKey进行比较,如果相等,则发送到该Binding对应的Queue中。

b) 如果是Fanout类型,则会将消息发送给所有与该Exchange定义过Binding的所有Queues中去,其实是一种广播行为。

c)如果是Topic类型,则会按照正则表达式,对RoutingKey与BindingKey进行匹配,如果匹配成功,则发送到对应的Queue中。

消费者订阅消息

在RabbitMQ中消费者有2种方式获取队列中的消息:

a) 一种是通过basic.consume命令,订阅某一个队列中的消息,channel会自动在处理完上一条消息之后,接收下一条消息。(同一个channel消息处理是串行的)。除非关闭channel或者取消订阅,否则客户端将会一直接收队列的消息。

b) 另外一种方式是通过basic.get命令主动获取队列中的消息,但是绝对不可以通过循环调用basic.get来代替basic.consume,这是因为basic.get RabbitMQ在实际执行的时候,是首先consume某一个队列,然后检索第一条消息,然后再取消订阅。如果是高吞吐率的消费者,最好还是建议使用basic.consume。

如果有多个消费者同时订阅同一个队列的话,RabbitMQ是采用循环的方式分发消息的,每一条消息只能被一个订阅者接收。例如,有队列Queue,其中ClientA和ClientB都Consume了该队列,MessageA到达队列后,被分派到ClientA,ClientA回复服务器收到响应,服务器删除MessageA;再有一条消息MessageB抵达队列,服务器根据“循环推送”原则,将消息会发给ClientB,然后收到ClientB的确认后,删除MessageB;等到再下一条消息时,服务器会再将消息发送给ClientA。

这里我们可以看出,消费者再接到消息以后,都需要给服务器发送一条确认命令,这个即可以在handleDelivery里显示的调用basic.ack实现,也可以在Consume某个队列的时候,设置autoACK属性为true实现。这个ACK仅仅是通知服务器可以安全的删除该消息,而不是通知生产者,与RPC不同。 如果消费者在接到消息以后还没来得及返回ACK就断开了连接,消息服务器会重传该消息给下一个订阅者,如果没有订阅者就会存储该消息。

既然RabbitMQ提供了ACK某一个消息的命令,当然也提供了Reject某一个消息的命令。当客户端发生错误,调用basic.reject命令拒绝某一个消息时,可以设置一个requeue的属性,如果为true,则消息服务器会重传该消息给下一个订阅者;如果为false,则会直接删除该消息。当然,也可以通过ack,让消息服务器直接删除该消息并且不会重传。

持久化:

Rabbit MQ默认是不持久队列、Exchange、Binding以及队列中的消息的,这意味着一旦消息服务器重启,所有已声明的队列,Exchange,Binding以及队列中的消息都会丢失。通过设置Exchange和MessageQueue的durable属性为true,可以使得队列和Exchange持久化,但是这还不能使得队列中的消息持久化,这需要生产者在发送消息的时候,将delivery mode设置为2,只有这3个全部设置完成后,才能保证服务器重启不会对现有的队列造成影响。这里需要注意的是,只有durable为true的Exchange和durable为true的Queues才能绑定,否则在绑定时,RabbitMQ都会抛错的。持久化会对RabbitMQ的性能造成比较大的影响,可能会下降10倍不止。

事务

对事务的支持是AMQP协议的一个重要特性。假设当生产者将一个持久化消息发送给服务器时,因为consume命令本身没有任何Response返回,所以即使服务器崩溃,没有持久化该消息,生产者也无法获知该消息已经丢失。如果此时使用事务,即通过txSelect()开启一个事务,然后发送消息给服务器,然后通过txCommit()提交该事务,即可以保证,如果txCommit()提交了,则该消息一定会持久化,如果txCommit()还未提交即服务器崩溃,则该消息不会服务器就收。当然Rabbit MQ也提供了txRollback()命令用于回滚某一个事务。

confirm机制

使用事务固然可以保证只有提交的事务,才会被服务器执行。但是这样同时也将客户端与消息服务器同步起来,这背离了消息队列解耦的本质。Rabbit MQ提供了一个更加轻量级的机制来保证生产者可以感知服务器消息是否已被路由到正确的队列中——Confirm。如果设置channel为confirm状态,则通过该channel发送的消息都会被分配一个唯一的ID,然后一旦该消息被正确的路由到匹配的队列中后,服务器会返回给生产者一个Confirm,该Confirm包含该消息的ID,这样生产者就会知道该消息已被正确分发。对于持久化消息,只有该消息被持久化后,才会返回Confirm。Confirm机制的最大优点在于异步,生产者在发送消息以后,即可继续执行其他任务。而服务器返回Confirm后,会触发生产者的回调函数,生产者在回调函数中处理Confirm信息。如果消息服务器发生异常,导致该消息丢失,会返回给生产者一个nack,表示消息已经丢失,这样生产者就可以通过重发消息,保证消息不丢失。Confirm机制在性能上要比事务优越很多。但是Confirm机制,无法进行回滚,就是一旦服务器崩溃,生产者无法得到Confirm信息,生产者其实本身也不知道该消息吃否已经被持久化,只有继续重发来保证消息不丢失,但是如果原先已经持久化的消息,并不会被回滚,这样队列中就会存在两条相同的消息,系统需要支持去重。

1.什么情况会导致 blackhole?

两种情况:

声明 exchange 后未绑定任何 queue ,此时发送到该 exchange 上的 message 均被 blackholed ;
声明 exchange 后绑定了 queue ,但发送到该 exchange 上的 message 所使用的 routing_key 不匹配任何 binding_key ,则 blackholed 。

本文转自说说常见搜索引擎的分布式解决方法
随着索引数据的增大以及请求的增多,分布式搜索是最好的一种解决方案,主要解决两个问题,其一是能让单台机器load所有索引数据到内存中,其二是请求延时大,解决请求latency问题。我之前为团队写了篇专利,内容是关于分布式搜索解决方案的,所以粗略的看了下大部分开源的搜索引擎是怎么实现分布式的,后面的文章我会简单说下常见的搜索引擎的分布式解决方案。

首先我们先说下几个简单概念,分布式搜索都是M*N(横向切分数据,纵向切分流量)这个维度去解决问题的,虽然不同产品或场景概念不完全相同,读者可以简单认为一份完整的数据,被均分为M份,每一份被称为一个分配(Shard或者Partition),然后提供对每个Shard提供N份副本(Replica)。那么分布式的设计就围绕着以下问题:

  • 如何选择合适的分片(Shard),副本(Replica)的数量
  • 如何做路由,即怎么在所有Shard里找到一份完整的数据(找到对应的机器列表)
  • 如何做负载均衡
  • 如果提高服务的可扩展性
  • 如何提高服务的服务能力(QPS),当索引和搜索并发量增大时,如何平滑解决
  • 如何更新索引,全量和增量索引的更新解决方法
  • 如果提高服务的稳定性,单台服务挂掉怎么不影响整体服务等等

下面就说下常见的搜索引擎的分布式解决方案,因为开源的搜索产品基本上都没有在工作中用过,对代码细节并不是太了解,只是研究了下其原理,所以理解的会有些偏差,看官们如果发现错误直接指出即可。

Sphinx/Coreseek

Sphinx的流程还是很简单的,可以看下其流程图:
sphinx
需要支持分布式的话,需要改下配置,大致是这样子的:

1
2
3
4
5
6
7
8
index dist
{
type = distributed
local = chunk1
agent = localhost:9312:chunk2 本地
agent = 192.168.100.2:9312:chunk3 远程
agent = 192.168.100.3:9312:chunk4 远程
}

从图中也可以看出,需要在配置列表里配置好其他shard的地址。查询过程为:

  • 连接到远程代理
  • 执行查询
  • 对本地索引进行查询
  • 接收来自远程代理的搜索结果
  • 将所有结果合并,删除重复项
  • 将合并后的结果返回给客户端
    索引数据复制同步的方法也是常用的两种:

主从同步
增量更新索引
方法也是设置crontab,添加2个选项,一个是重建主索引,一个是增量索引更新。

当然为了避免单点以及增加服务能力,肯定有多个Replica,解决方法应该也是配置或者haproxy相关的方法解决,从上面可以看出,Sphinx很难用,自动化能力太弱,所以很多大厂要么不再使用Sphinx要么基于其二次开发。

Solr

Solr提供了两种方案来应对访问压力,其一是Replication,另一个是SolrCloud。我们此处只说Replication原理。
Replication采用了Master/Slave模式,也就是说由一个主索引和多个从索引构成,从索引从主索引复制索引,主索引负责更新索引,从索引负责同步索引和查询。本质上是读写分离的思想,MySQL/Redis等数据库也多是这种方式部署的。有两种部署方式:

  • 第一种
    solr-replica
  • 第二种
    solr-repeater

与第一种相比多了一层Repeater,Repeater既扮演了Master角色,又扮演了Slave功能,主要解决单个Master下Slave太多,Master压力太大的问题。

Master与Slave之间的通信是无状态的http连接,Slave端发送不同的Command从Master端获得数据。原理就是Master那边有个标志位和版本号,用于获取正确的数据版本,然后数据扔到Slave临时目录下,数据完整后,再覆盖原有数据。多个副本的方法应该与Sphinx相似,一般也是通过通过上游负载均衡模块如Nginx,HaProxy来分流。

SolrCloud

因为Solr Replication不好用,本质上还不算真正分布式的,所以Solr从4.0开始支持SolrCloud模式。特性不少,主要说两个吧:

配置文件统一管理,扔到Zookeeper上
自动做负载均衡和故障恢复,不再需要Nginx或HaProxy的支持
逻辑图
逻辑图

  • Collection:逻辑意义上一份完整的索引
  • Shard:上文已说,就是索引的1/N分片
  • Replica:Shard的一个拷贝
    每个Shard,即相同的Replica下都会有一个leader,leader选举由Zookeeper完成。虽然有leader的概念,但是其实SolrCloud分布式是去中心化的,意思就是说,leader和非leader都能提供查询功能(也有修改和删除功能,搜索场景应用不多吧?),而更新索引,创建Collection/Shard/Replica(即扩容)只能由leader完成,避免产生并发修改问题,当非leader节点收到修改操作请求时,将信息存储在zookeeper中相应节点上,leader节点会一直对该节点进行watch,发现变化就实时做处理。

创建索引

任意节点收到创建索引请求后,转换成json格式存储到zk的/overseer/collection-queue-work的children节点上
leader线程一直监控collection-queue-work节点,检查到变化后,取出json数据,根据信息计算出需要创建的shard、replica,将创建具体replica的请求转向各对应节点
各节点创建完具体的replica后,将该节点的状态(创建成功与否等)更新到/overseer/queue的children节点上
leader线程监控/overseer/queue节点,将overseer/queue的children节点的状态更新至/clusterstate.json
各节点同步/clusterstate.json,整个集群状态得到更新,新索引创建成功

更新索引

根据路由规则计算出该doc所属shard,并找出该doc所属的shard对应的leader
如果当前Replica是对应Shard且是leader,首先更新本地索引,然后再将doc转向该Shard的其余Replica
扩容/缩容

停掉某台Solr,更新集群状态到/clusterstate.json
增加一台Solr,从leader出复制相同的数据,然后配置写到/clusterstate.json
查询

去中心的,leader和非leader一样功能,Replica接收搜索请求时,从Zookeeper中获取该Replica对应的Collection及所有的Shard和Replica
将请求发送到该Collection下对应的Shard,然后负载均衡到对应的Replica
SolrCloud也有其他功能,比如Optimization,就是一个运行在leader机器的进程,复杂压缩索引和归并Segment;近实时搜索等。总体看SolrCloud解决了Solr Replication遇到的一些问题,比Sphinx更好用,更自动化。

一号店

很多大一点的厂商如果不自研搜索引擎的话,并没有使用SolrCloud,而多基于Solr/Lucence。比如一号店的分布式搜索解决方案,如下所示:
http://www.infoq.com/cn/articles/yhd-11-11-distributed-search-engine-architecture

Broker就相当于Proxy,扮演了路由功能,很多厂商做的与一号店有相似之处。因为没有leader选举,所以索引的更新就由其他模块来做了。

ElasticSearch

ElasticSearch的倒排索引也是基于Lucence实现的。功能强大,不仅提供了实时搜索功能,还有分析功能,DB-Engines上面的搜索引擎排名,目前已经超越Solr排名第一位了。因为太强大了,功能也特别多,我研究还不够深,简单说下吧。
es会将集群名字相同的机器归为一个集群(业务),所以先说下启动过程。

启动过程

es_start
当ElasticSearch的节点启动后,它会利用多播(multicast)(或者单播,如果用户更改了配置)寻找集群中的其它节点,并与之建立连接。

leader选举

与SolrCloud相似,也是去中心化的,但是没有使用Zookeeper,而是自己实现了分布式锁,选主的流程叫做 ZenDiscovery(详情见第三个参考链接):

  • 节点启动后先ping(这里的ping是 Elasticsearch 的一个RPC命令。如果 discovery.zen.ping.unicast.hosts 有设置,则ping设置中的host,否则尝试ping localhost 的几个端口, Elasticsearch 支持同一个主机启动多个节点)
  • Ping的response会包含该节点的基本信息以及该节点认为的master节点
  • 选举开始,先从各节点认为的master中选,规则很简单,按照id的字典序排序,取第一个
  • 如果各节点都没有认为的master,则从所有节点中选择,规则同上。这里有个限制条件就是 discovery.zen.minimum_master_nodes,如果节点数达不到最小值的限制,则循环上述过程,直到节点数足够可以开始选举
  • 最后选举结果是肯定能选举出一个master,如果只有一个local节点那就选出的是自己
  • 如果当前节点是master,则开始等待节点数达到 minimum_master_nodes,然后提供服务
  • 如果当前节点不是master,则尝试加入master
  • 选举完leader后,主节点leader会去读取集群状态信息;因为主节点会监控其他节点,当其他节点出现故障时,会进行恢复工作。在这个阶段,主节点会去检查哪些分片可用,决定哪些分片作为主分片。

选举完leader后,主节点leader会去读取集群状态信息;因为主节点会监控其他节点,当其他节点出现故障时,会进行恢复工作。在这个阶段,主节点会去检查哪些分片可用,决定哪些分片作为主分片。

分片

es在创建索引时,自己设置好分片个数,默认5个,整个过程类似于分裂的概念,如下图所示:

es_shard

至于读写、写操作等于SolrCloud相似,等我细研究后后续再说吧,也可以说下实时索引怎么做的,细节很多,下次再说吧。至于文中为什么不说Lucence,因为Lucence其实就是个index lib,只是解决倒排、正排索引怎么存放的,并不是一个完整的搜索引擎解决方案。而ES为什么能脱颖而出的主要原因是配套设施完善,工具,Web UI都是非常赞的,对于很多开源产品,它能后来居上的主要原因就是它真实的能解决用户遇到的问题或者比其他产品更好用。搜索引擎发展这么多年了,架构这块能做的基本上大家都差不太多,最后能脱颖而出的肯定是第三方工具做的更完善,更好用的了。

PS:
至于阿里搜索怎么做的,可以参考下这个文档,包括了阿里搜索里用到的很多基础模块了:
https://share.weiyun.com/f66e79d9f6897d0aac683361531cf00d

参考链接:
http://blog.haohtml.com/archives/13724
http://www.voidcn.com/blog/u011026968/article/p-4922079.html
http://jolestar.com/elasticsearch-architecture/

linux 下c语言编程

工具

GCC(GNU Compiler Collection,GNU编译器套装),是一套由 GNU 开发的编程语言编译器。它是一套以 GPL 及 LGPL 许可证所发行的自由软件,也是 GNU计划 的关键部分,亦是自由的 类Unix 及苹果计算机 Mac OS X 操作系统的标准编译器。GCC(特别是其中的C语言编译器)也常被认为是跨平台编译器的事实标准。在ubuntu等操作系统中编译安装大多数开源项目几乎都会用到gcc。

编译

1
2
3
4
5
6
7
//hellp.c
#include <stdio.h>
int main(void)
{
printf("Hello World!\n");
return 0;
}

简单的编译: gcc hello.c -o hello
gcc 编译器就会为我们生成一个 hello 的可执行文件,执行./hello 就可以看到程序的输出结果了。命令行中 gcc 表示我们是用 gcc 来编译我们的源程序,-o 选项表示我们要求编译器 给我们输出的可执行文件名为 hello 而 hello.c 是我们的源程序文件。
实质上,上述编译过程是分为四个阶段进行的,即预处理(也称预编译,Preprocessing)、编译(Compilation)、汇编 (Assembly)和连接(Linking)。

gcc还有很多参数:
-o 要求输出的可执行文件名。
-c 要求编译器输出目标代码,而不必要输出可执行文件。
-g 要求编译器在编译的时候提供我们以后对程序进行调试的信息。

多个程序文件的编译

比如hello.c include了一些头,包含在hello1.h和hello2.h中,hello1.c和hello2.c也包含了这些头,需要编译多个文件
gcc -c hello.c
gcc -c hello1.c
gcc -c hello2.c
gcc hello.o hello1.o hello2.o -o hello

虽然这些命令可以使用shell脚本完成,但是大型的项目中会有很多源程序,不可能有一个改动就重新一个个编译。
因此就有了makefile,makefile带来的好处就是自动化编译,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。

makefile

在 Makefile 中也#开始的行都是注释行.Makefile 中 最重要的是描述文件的依赖关系的说明.一般的格式是:
target: prerequisites
TAB command
第一行表示的是依赖关系,第二行是规则。

于是 hello的生成可以这么写

hello:hello.o hello1.o hello2.o
gcc -o hello hello.o hello1.o hello2.o
hello.o:hello.c hello1.h hello2.h
gcc -c hello.c
hello1.o:hello1.c hello1.h
gcc -c hello1.c
hello2.o:hello2.c hello2.h
gcc -c hello2.c

Makefile 有三个非常有用的变量.分别是$@,$^,$<代表的意义分别是: $@–目标文件,$^–所有的依赖文件,$<–第一个依赖文件。
所以上面的可以写成
hello:hello.o hello1.o hello2.o
gcc -o $@ $^
.c.o:
gcc -c $<

调试

gdb工具,暂时没用到,先不看了

简单地了解下linux下C语言编程。有空得继续深入了

分布式id生成器

概念

有些人也喜欢叫发号器,一般是作为服务运行,主要目的是就是为一个系统的数据对象分配一个唯一id。

特点

  • 唯一性
  • 时间段内粗略有序
  • 可制造
  • 可反解

一般来说,id生成器的要求主要是一下几点:

  • 快速响应,可以毫秒级并发
  • 高可用性
  • 体积小(存储)

这里不讨论uuiduuid-vs-int-insert-performance

生成方法

自增有序

这种方式可以利用mysql的自增id。但是使用mysql有一些瓶颈:

  • 单点问题(高可用性)
  • 并发

flickr巧妙的利用replace实现分布式发号器
自增id使用big int 为了提高并发,使用多台mysql机器,每台机器设置不同的起始值和步长,sql使用replace唯一id的方式使得mysql每次增长一个步长(insert on duplacate key也可以,update不可以,是因为update的返回值是成功还是失败)
表结构

1
2
3
4
5
6
CREATE TABLE `Tickets64` (
`id` bigint(20) unsigned NOT NULL auto_increment,
`stub` char(1) NOT NULL default '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=MyISAM

sql语句

1
2
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();

设置步长和起始值(步长也可以直接在session中指定)

1
2
3
4
5
6
7
TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1
TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2

php 实现,java无法使用jpa,可以用jdbcTemplate实现

1
2
3
4
5
6
7
8
9
10
<?php
include_once 'pdoFactory.php';
// insert
//$sql = "INSERT INTO Tickets64 (stub) VALUES ('a') ON DUPLiCATE KEY UPDATE id=id+1";
// replace
$sql = "REPLACE INTO Tickets64 (stub) VALUES ('a')";
$pdo = pdoFactory::getInstance()->getPdo('default');
$p = $pdo->exec($sql);
echo "<pre>";print_r($pdo->lastInsertId());exit;

优缺点

优点是简单,实现快速。

缺点是mysql中需要设置起始值,需要多台mysql服务,可扩展性差。并发取决于mysql节点数量

架构图

腾讯的seqsvr 万亿级调用系统:微信序列号生成器架构设计及演变
简单来讲,seqsvr是一个巨大的long数组,每个元素代表一个用户,比如小明的uid是12,那么seq[12]就是他产生的guid,每个用户(元素)之间产生的uid互不影响,每个uid产生的guid严格递增(为了保证这一点,付出的代价很高,不能使用负载均衡,同一时间只有一个服务器产生该用户的guid),发号分为两层,下层是持久化层,主要存储当前uid产生的最大的guid,上层为缓存层,缓存一个步长,用来发号。也做了很多优化,比如设置最大临时guid,相邻的uid共享该max_guid,这样在服务器重启时可以大大减少向存储层发起的请求,减少内存使用。容灾问题上做了很多处理,前面说到这种方式存在单点问题,seqsrv做了很多,分set做隔离,路由和配置服务做失败请求,仲裁服务做服务切换

时间有序

主流的方式都是类似twitter的snowflake,一个long是64字节,可以根据自身业务特点选择秒级别还是毫秒级别,一般系统寿命在30年左右,所以毫秒级别需要41位表示时间,秒级别需要30位,剩下的需要几位去表示机器号,也可以预留2位表示版本,方便切换

摘自一乐:业务系统需要什么样的ID生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SnowFlake
41bit留给毫秒时间,10bit给MachineID,也就是机器要预先配置,剩下12位留给Sequence。
Weibo
微博使用了秒级的时间,用了30bit,Sequence 用了15位,理论上可以搞定3.2w/s的速度。用4bit来区分IDC,也就是可以支持16个 IDC,对于核心机房来说够了。剩下的有2bit 用来区分业务,由于当前发号服务是机房中心式的,1bit 来区分热备。是的,也没有用满64bit。
Ticktick
也就是当前在环信系统里要用到的。使用了30bit 的秒级时间,20bit 给Sequence。这里是有个考虑,第一版实现还是希望到毫秒级,所以20bit 的前10bit给了毫秒来用,剩下10bit给 Sequence。等到峰值提高的时候可以暂时回到秒级。
前面说到的三十年问题,因此我在高位留了2bit 做 Version,或者到时候改造使用更长字节数,用第一位来标识不同 ID,或者可以把这2bit 挪给时间用,可以给系统改造留出一定的时间。
剩下的10bit 留给 MachineID,也就是说当前 ID 生成可以直接内嵌在业务服务中,最多支持千级别的服务器数量。最后有2bit 做Tag 用,可能区分群消息和单聊消息。同时你也看出,这个 ID 最多支持一天10亿消息,也是怕系统增速太快,这2bit 可以挪给 Sequence,可以支持40亿级别消息量,或者结合前面的版本支持到百亿级别。
修正:评论里指出上面一个计算错误,不挪借的话应该是支持一天约千亿级别。对比当前 Whatsapp 的600亿和腾讯 QQ 的200亿,已经足够了。

snowflake

优点:可用性强

缺点:依赖zookeeper,要么人肉做配置,总之需要在服务中配置机器号识别。

Instagram
sharding-ids-at-instagram

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE OR REPLACE FUNCTION insta5.next_id(OUT result bigint) AS $$
DECLARE
our_epoch bigint := 1314220021721;
seq_id bigint;
now_millis bigint;
shard_id int := 5;
BEGIN
SELECT nextval('insta5.table_id_seq') %% 1024 INTO seq_id;
SELECT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000) INTO now_millis;
result := (now_millis - our_epoch) << 23;
result := result | (shard_id << 10);
result := result | (seq_id);
END;
$$ LANGUAGE PLPGSQL;

也是类似snowflake,直接在postgrsql中实现的。简单看了一下,类似触发器,id自增换成函数生成
达达的也类似达达-高性能服务端优化之路

优: 开发成本低
劣: 基于postgreSQL的存储过程,较为偏门

基于redis的id生成器

基于redis的id生成器(http://blog.csdn.net/hengyunabc/article/details/44244951)

优点:利用redis lua脚本实现,少量机器即可满足需求

缺点: 需要配置redis起始值及步长

一些变种

Hibernate github

1
2
3
4
5
6
- 时间戳(6bytes, 48bit):毫秒级别的,从1970年算起,能撑8925年....
- 顺序号(2bytes, 16bit, 最大值65535): 没有时间戳过了一秒要归零的事,各搞各的,short溢出到了负数就归0。
- 机器标识(4bytes 32bit): 拿localHost的IP地址,IPV4呢正好4个byte,但如果是IPV6要16个bytes,就只拿前4个byte。
- 进程标识(4bytes 32bit): 用当前时间戳右移8位再取整数应付,不信两条线程会同时启动。
值得留意就是,机器进程和进程标识组成的64bit Long几乎不变,只变动另一个Long就够了。

mongodb Mongdodb objectId 非long型

1
2
3
4
5
6
7
8
9
- 时间戳(4 bytes 32bit): 是秒级别的,从1970年算起,能撑136年。
- 自增序列(3bytes 24bit, 最大值一千六百万): 是一个从随机数开始(机智)的Int不断加一,也没有时间戳过了一秒要归零的事,各搞各的。因为只有3bytes,所以一个4bytes的Int还要截一下后3bytes。
- 机器标识(3bytes 24bit): 将所有网卡的Mac地址拼在一起做个HashCode,同样一个int还要截一下后3bytes。搞不到网卡就用随机数混过去。
- 进程标识(2bytes 16bits):从JMX里搞回来到进程号,搞不到就用进程名的hash或者随机数混过去。
可见,MongoDB的每一个字段设计都比Hibernate的更合理一点,比如时间戳是秒级别的。总长度也降到了12 bytes 96bit,但如果果用64bit长的Long来保存有点不上不下的,只能表达成byte数组或16进制字符串。

参考

江南白衣:分布式Unique ID的生成方法一览

一乐:业务系统需要什么样的ID生成器

flickr Ticket Servers: Distributed Unique Primary Keys on the Cheap

利用shell脚本对大文件进行分割

日常应用产生的日志是需要做文件分割的,无论是按文件大小还是按时间

## 利用shell脚本

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
lines=`wc -l xxx.log| awk '{print $1}'`
i=1
file_suffix=1
while [ $i -lt $lines ]
do
next=`expr $i + 999`
sed -n "${i}, ${next}p" xxx.log > file_$file_suffix.log
n1=`expr $n2 + 1`
file_suffix=`expr $file_suffix + 1`
done

实现了每1000行来分割文件

使用工具split

split 参数:
-b : 后面可接欲分割成的档案大小,可加单位,例如 b, k, m 等;
-l : 以行数来进行分割;
-d : 默认是以字符为后缀的,使用-d 或者–numeric-suffixes 可以改变后缀
查看参数更多可以 man split

1.按每个文件1000行来分割除
split -l 1000 access.log split
会切割成splitaa splitab splitac…

2.按照每个文件100K来分割
split -l 1000 access.log split
会切割成httpaa,httpab,httpac ……..

3.流切割
对于流split一样可以做到
简单点举例
cat access.log | split -l 1000 split
这样有些守护进程就可以这样写
php xxx.php | split -l 10000 xxx.log.

数据结构之并查集

文章转自数据结构之并查集;

概述

并查集(Disjoint set或者Union-find set)是一种树型的数据结构,常用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。

基本操作

并查集是一种非常简单的数据结构,它主要涉及两个基本操作,分别为:

  • 合并两个不相交集合

  • 判断两个元素是否属于同一个集合

1.合并两个不相交集合(Union(x,y))

合并操作很简单:先设置一个数组Father[x],表示x的“父亲”的编号。那么,合并两个不相交集合的方法就是,找到其中一个集合最父亲的父亲(也就是最久远的祖先),将另外一个集合的最久远的祖先的父亲指向它。
union

上图为两个不相交集合,b图为合并后Father(b):=Father(g)

2.判断两个元素是否属于同一集合(Find_Set(x))

本操作可转换为寻找两个元素的最久远祖先是否相同。可以采用递归实现。

优化

1.Find_Set(x)时,路径压缩

寻找祖先时,我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度。为了避免这种情况,我们需对路径进行压缩,即当我们经过”递推”找到祖先节点后,”回溯”的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了,如下图所示。可见,路径压缩方便了以后的查找。

2.Union(x,y)时,按秩合并

即合并的时候将元素少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。

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
int father[MAX]; /* father[x]表示x的父节点*/
int rank[MAX]; /*rank[x]表示x的秩*/
&nbsp;
void Make_Set(int x)
{
father[x] = x; //根据实际情况指定的父节点可变化
rank[x] = 0; //根据实际情况初始化秩也有所变化
}
/* 查找x元素所在的集合,回溯时压缩路径*/
int Find_Set(int x)
{
if (x != father[x])
{
father[x] = Find_Set(father[x]); //这个回溯时的压缩路径是精华
}
return father[x];
}
/*
按秩合并x,y所在的集合
下面的那个if else结构不是绝对的,具体根据情况变化
但是,宗旨是不变的即,按秩合并,实时更新秩。
*/
void Union(int x, int y)
{
x = Find_Set(x);
y = Find_Set(y);
if (x == y) return;
if (rank[x] > rank[y])
{
father[y] = x;
}
else
{
if (rank[x] == rank[y])
{
rank[y]++;
}
father[x] = y;
}
}

复杂度分析

空间复杂度为O(N),建立一个集合的时间复杂度为O(1),N次合并M查找的时间复杂度为O(M Alpha(N)),这里Alpha是Ackerman函数的某个反函数,在很大的范围内(人类目前观测到的宇宙范围估算有10的80次方个原子,这小于前面所说的范围)这个函数的值可以看成是不大于4的,所以并查集的操作可以看作是线性的。具体复杂度分析过程见参考资料(3)。

应用

并查集常作为另一种复杂的数据结构或者算法的存储结构。常见的应用有:求无向图的连通分量个数,最近公共祖先(LCA),带限制的作业排序,实现Kruskar算法求最小生成树等。

参考资料

文章转自SHELL编程之内建命令

微博ID:orroz

微信公众号:Linux系统技术

前言

本文是shell编程系列的第五篇,集中介绍了bash相关内建命令的使用。通过学习本文内容,可以帮你解决以下问题:

  1. 什么是内建命令?为什么要有内建命令?
  2. 为啥echo 111 222 333 444 555| read -a test之后echo ${test[*]}不好使?
  3. ./script和. script有啥区别?
  4. 如何让让kill杀不掉你的bash脚本?
  5. 如何更优雅的处理bash的命令行参数

为什么要有内建命令

内建命令是指bash内部实现的命令。bash在执行这些命令的时候不同于一般外部命令的fork、exec、wait的处理过程,这内建功能本身不需要打开一个子进程执行,而是bash本身就可以进行处理。分析外部命令的执行过程我们可以理解内建命令的重要性,外建命令都会打开一个子进程执行,所以有些功能没办法通过外建命令实现。比如当我们想改变当前bash进程的某些环境的时候,如:切换当前进程工作目录,如果打开一个子进程,切换之后将会改变子进程的工作目录,与当前bash没关系。所以内建命令基本都是从必须放在bash内部实现的命令。bash所有的内建命令只有50多个,绝大多数的命令我们在之前的介绍中都已经使用过了。下面我们就把它们按照使用的场景分类之后,分别介绍一下在bash编程中可能会经常用到的内建命令。

输入输出

对于任何编程语言来说,程序跟文件的输入输出都是非常重要的内容,bash编程当然也不例外。所有的shell编程与其他语言在IO处理这一块的最大区别就是,shell可以直接使用命令进行处理,而其他语言基本上都要依赖IO处理的库和函数进行处理。所以对于shell编程来说,IO处理的相关代码写起来要简单的多。本节我们只讨论bash内建的IO处理命令,而外建的诸如grep、sed、awk这样的高级处理命令不在本文的讨论范围内。

source:

.:

以上两个命令:source和.实际上是同一个内建命令,它们的功能完全一样,只是两种不同写法。我们都应该见过这样一种写法,如:

1
2
3
4
5
6
7
8
9
for i in /etc/profile.d/*.sh; do
if [ -r "$i" ]; then
if [ "$PS1" ]; then
. "$i"
else
. "$i" >/dev/null 2>&1
fi
fi
done

这里的”. $i”实际上就是source $i。这个命令的含义是:读取文件的内容,并在当前bash环境下将其内容当命令执行。注意,这与输入一个可执行脚本的路径的执行方式是不同的。路径执行的方式会打开一个子进程的bash环境去执行脚本中的内容,而source方式将会直接在当前bash环境中执行其内容。所以这种方式主要用于想引用一个脚本中的内容用来改变当前bash环境。如:加载环境变量配置脚本或从另一个脚本中引用其定义的函数时。我们可以通过如下例子来理解一下这个内建命令的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[zorro@zorrozou-pc0 bash]$ cat source.sh
#!/bin/bash
aaa=1000
echo $aaa
echo $$
[zorro@zorrozou-pc0 bash]$ ./source.sh
1000
27051
[zorro@zorrozou-pc0 bash]$ echo $aaa
[zorro@zorrozou-pc0 bash]$ . source.sh
1000
17790
[zorro@zorrozou-pc0 bash]$ echo $aaa
1000
[zorro@zorrozou-pc0 bash]$ echo $$
17790

我们可以通过以上例子中的$aaa变量看到当前bash环境的变化,可以通过$$变量,看到不同执行过程的进程环境变化。

read:

这个命令可以让bash从标准输入读取输字符串到一个变量中。用法如下:

1
2
3
4
5
6
7
8
9
10
[zorro@zorrozou-pc0 bash]$ cat input.sh
#!/bin/bash
read -p "Login: " username
read -p "Passwd: " password
echo $username
echo $password

程序执行结果:

1
2
3
4
5
[zorro@zorrozou-pc0 bash]$ ./input.sh
Login: zorro
Passwd: zorro
zorro
zorro

我们可以利用read命令实现一些简单的交互程序。read自带提示输出功能,-p参数可以让read在读取输入之前先打印一个字符串。read命令除了可以读取输入并赋值一个变量以外,还可以赋值一个数组,比如我们想把一个命令的输出读到一个数组中,使用方法是:

1
2
3
4
5
6
7
[zorro@zorrozou-pc0 bash]$ cat read.sh
#!/bin/bash
read -a test
echo ${test[*]}

执行结果:

1
2
3
[zorro@zorrozou-pc0 bash]$ ./read.sh
111 222 333 444 555
111 222 333 444 555

输入为:111 222 333 444 555,就会打印出整个数组列表。

mapfile:

readarray:

这两个命令又是同一个命令的两种写法。它们的功能是,将一个文本文件直接变成一个数组,每行作为数组的一个元素。这对某些程序的处理是很方便的。尤其是当你要对某些文件进行全文的分析或者处理的时候,比一行一行读进来处理方便的多。用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[zorro@zorrozou-pc0 bash]$ cat mapfile.sh
#!/bin/bash
exec 3< /etc/passwd
mapfile -u 3 passwd
exec 3<&-
echo ${#passwd}
for ((i=0;i<${#passwd};i++))
do
echo ${passwd[$i]}
done

程序输出:

1
2
3
4
5
6
[zorro@zorrozou-pc0 bash]$ ./mapfile.sh
32
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/usr/bin/nologin
daemon:x:2:2:daemon:/:/usr/bin/nologin
...

本例子中使用了-u参数,表示让mapfile或readarray命令从一个文件描述符读取,如果不指定文件描述符,命令将默认从标准输入读取。所以很多人可能习惯用管道的方式读取,如:

1
2
[zorro@zorrozou-pc0 bash]$ cat /etc/passwd|mapfile passwd
[zorro@zorrozou-pc0 bash]$ echo ${passwd[*]}

但是最后却发现passwd变量根本不存在。这个原因是:如果内建命令放到管道环境中执行,那么bash会给它创建一个subshell进行处理。于是创建的数组实际上与父进程没有关系。这点是使用内建命令需要注意的一点。同样,read命令也可能会出现类似的使用错误。如:

1
echo 111 222 333 444 555| read -a test

执行完之后,我们在bash脚本环境中仍然无法读取到test变量的值,也是同样的原因。

mapfile的其他参数,大家可以自行参考help mapfile或help readarray取得帮助。

echo:

printf:

这两个都是用来做输出的命令,其中echo是我们经常使用的,就不啰嗦了,具体参数可以help echo。printf命令是一个用来进行格式化输出的命令,跟C语言或者其他语言的printf格式化输出的方法都类似,比如:

1
2
[zorro@zorrozou-pc0 bash]$ printf "%d\t%s %f\n" 123 zorro 1.23
123 zorro 1.230000

使用很简单,具体也请参见:help printf。

作业控制

作业控制指的是jobs功能。一般情况下bash执行命令的方式是打开一个子进程并wait等待其退出,所以bash在等待一个命令执行的过程中不能处理其他命令。而jobs功能给我们提供了一种办法,可以让bash不用显示的等待子进程执行完毕后再处理别的命令,在命令行中使用这个功能的方法是在命令后面加&符号,表明进程放进作业控制中处理,如:

1
2
3
4
5
6
7
8
9
10
[zorro@zorrozou-pc0 bash]$ sleep 3000 &
[1] 30783
[zorro@zorrozou-pc0 bash]$ sleep 3000 &
[2] 30787
[zorro@zorrozou-pc0 bash]$ sleep 3000 &
[3] 30791
[zorro@zorrozou-pc0 bash]$ sleep 3000 &
[4] 30795
[zorro@zorrozou-pc0 bash]$ sleep 3000 &
[5] 30799

我们放了5个sleep进程进入jobs作业控制。大家可以当作这是bash提供给我们的一种“并发处理”方式。此时我们可以使用jobs命令查看作业系统中有哪些进程在执行:

1
2
3
4
5
6
[zorro@zorrozou-pc0 bash]$ jobs
[1] Running sleep 3000 &
[2] Running sleep 3000 &
[3] Running sleep 3000 &
[4]- Running sleep 3000 &
[5]+ Running sleep 3000 &

除了数字外,这里还有+和-号标示。+标示当前作业任务,-表示备用的当前作业任务。所谓的当前作业,就是最后一个被放到作业控制中的进程,而备用的则是当前进程如果退出,那么备用的就会变成当前的。这些jobs进程可以使用编号和PID的方式控制,如:

1
2
3
4
5
6
7
[zorro@zorrozou-pc0 bash]$ kill %1
[1] Terminated sleep 3000
[zorro@zorrozou-pc0 bash]$ jobs
[2] Running sleep 3000 &
[3] Running sleep 3000 &
[4]- Running sleep 3000 &
[5]+ Running sleep 3000 &

表示杀掉1号作业任务,还可以使用kill %+或者kill %-以及kill %%(等同于%+)。除了可以kill这些进程以外,bash还提供了其他控制命令:

fg:
bg:

将指定的作业进程回到前台让当前bash去wait。如:

1
2
[zorro@zorrozou-pc0 bash]$ fg %5
sleep 3000

于是当前bash又去“wait”5号作业任务了。当然fg后面也可以使用%%、%+、%-等符号,如果fg不加参数效果跟fg %+也是一样的。让一个当前bash正在wait的进程回到作业控制,可以使用ctrl+z快捷键,这样会让这个进程处于stop状态:

[zorro@zorrozou-pc0 bash]$ fg %5
sleep 3000
^Z
[5]+ Stopped sleep 3000

[zorro@zorrozou-pc0 bash]$ jobs
[2] Running sleep 3000 &
[3] Running sleep 3000 &
[4]- Running sleep 3000 &
[5]+ Stopped sleep 3000

这个进程目前是stopped的,想让它再运行起来可以使用bg命令:

1
2
3
4
5
6
7
[zorro@zorrozou-pc0 bash]$ bg %+
[5]+ sleep 3000 &
[zorro@zorrozou-pc0 bash]$ jobs
[2] Running sleep 3000 &
[3] Running sleep 3000 &
[4]- Running sleep 3000 &
[5]+ Running sleep 3000 &

disown:

disown命令可以让一个jobs作业控制进程脱离作业控制,变成一个“野”进程:

1
2
3
4
5
[zorro@zorrozou-pc0 bash]$ disown
[zorro@zorrozou-pc0 bash]$ jobs
[2] Running sleep 3000 &
[3]- Running sleep 3000 &
[4]+ Running sleep 3000 &

直接回车的效果跟diswon %+是一样的,也是处理当前作业进程。这里要注意的是,disown之后的进程仍然是还在运行的,只是bash不会wait它,jobs中也不在了。

信号处理

进程在系统中免不了要处理信号,即使是bash。我们至少需要使用命令给别进程发送信号,于是就有了kill命令。kill这个命令应该不用多说了,但是需要大家更多理解的是信号的概念。大家可以使用kill -l命令查看信号列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[zorro@zorrozou-pc0 bash]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

每个信号的意思以及进程接收到相关信号的默认行为了这个内容大家可以参见《UNIX环境高级编程》。我们在此先只需知道,常用的信号有2号(crtl c就是发送2号信号),15号(kill默认发送),9号(著名的kill -9)这几个就可以了。其他我们还需要知道,这些信号绝大多数是可以被进程设置其相应行为的,除了9号和19号信号。这也是为什么我们一般用kill直接无法杀掉的进程都会再用kill -9试试的原因。

那么既然进程可以设置信号的行为,bash中如何处理呢?使用trap命令。方法如下:

1
2
3
4
5
6
7
8
9
10
[zorro@zorrozou-pc0 bash]$ cat trap.sh
#!/bin/bash
trap 'echo hello' 2 15
trap 'exit 17' 3
while :
do
sleep 1
done

trap命令的格式如下:

1
trap [-lp] [[arg] signal_spec ...]

在我们的例子中,第一个trap命令的意思是,定义针对2号和15号信号的行为,当进程接收到这两个信号的时候,将执行echo hello。第二个trap的意思是,如果进程接收到3号信号将执行exit 17,以17为返回值退出进程。然后我们来看一下进程执行的效果:

1
2
3
4
5
6
7
8
[zorro@zorrozou-pc0 bash]$ ./trap.sh
^Chello
^Chello
^Chello
^Chello
^Chello
hello
hello

此时按ctrl+c和kill这个bash进程都会让进程打印hello。3号信号可以用ctrl+\发送:

1
2
3
4
5
6
7
8
9
10
11
[zorro@zorrozou-pc0 bash]$ ./trap.sh
^Chello
^Chello
^Chello
^Chello
^Chello
hello
hello
^\Quit (core dumped)
[zorro@zorrozou-pc0 bash]$ echo $?
17

此时进程退出,返回值是17,而不是128+3=131。这就是trap命令的用法。

suspend:

bash还提供了一种让bash执行暂停并等待信号的功能,就是suspend命令。它等待的是18号SIGCONT信号,这个信号本身的含义就是让一个处在T(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
[zorro@zorrozou-pc0 bash]$ cat suspend.sh
#!/bin/bash
pid=$$
echo "echo $pid"
#打开jobs control功能,在没有这个功能suspend无法使用,脚本中默认此功能关闭。
#我们并不推荐在脚本中开启此功能。
set -m
echo "Begin!"
echo $-
echo "Enter suspend stat:"
#让一个进程十秒后给本进程发送一个SIGCONT信号
( sleep 10 ; kill -18 $pid ) &
#本进程进入等待
suspend
echo "Get SIGCONT and continue running."
echo "End!"

执行效果:

1
2
3
4
5
6
7
[zorro@zorrozou-pc0 bash]$ ./suspend.sh
echo 31833
Begin!
hmB
Enter suspend stat:
[1]+ Stopped ./suspend.sh

十秒之后:

1
2
3
[zorro@zorrozou-pc0 bash]$
[zorro@zorrozou-pc0 bash]$ Get SIGCONT and continue running.
End!

以上是suspend在脚本中的使用方法。另外,suspend默认不能在非loginshell中使用,如果使用,需要加-f参数。

进程控制

bash中也实现了基本的进程控制方法。主要的命令有exit,exec,logout,wait。其中exit我们已经了解了。logout的功能跟exit实际上差不多,区别只是logout是专门用来退出login方式的bash的。如果bash不是login方式执行的,logout会报错:

1
2
3
4
5
6
[zorro@zorrozou-pc0 bash]$ cat logout.sh
#!/bin/bash
logout
[zorro@zorrozou-pc0 bash]$ ./logout.sh
./logout.sh: line 3: logout: not login shell: use `exit'

wait:

wait命令的功能是用来等待jobs作业控制进程退出的。因为一般进程默认行为就是要等待其退出之后才能继续执行。wait可以等待指定的某个jobs进程,也可以等待所有jobs进程都退出之后再返回,实际上wait命令在bash脚本中是可以作为类似“屏障”这样的功能使用的。考虑这样一个场景,我们程序在运行到某一个阶段之后,需要并发的执行几个jobs,并且一定要等到这些jobs都完成工作才能继续执行,但是每个jobs的运行时间又不一定多久,此时,我们就可以用这样一个办法:

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
[zorro@zorrozou-pc0 bash]$ cat wait.sh
#!/bin/bash
echo "Begin:"
(sleep 3; echo 3) &
(sleep 5; echo 5) &
(sleep 7; echo 7) &
(sleep 9; echo 9) &
wait
echo parent continue
sleep 3
echo end!
[zorro@zorrozou-pc0 bash]$ ./wait.sh
Begin:
3
5
7
9
parent continue
end!

通过这个例子可以看到wait的行为:在不加任何参数的情况下,wait会等到所有作业控制进程都退出之后再回返回,否则就会一直等待。当然,wait也可以指定只等待其中一个进程,可以指定pid和jobs方式的作业进程编号,如%3,就变成了:

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
[zorro@zorrozou-pc0 bash]$ cat wait.sh
#!/bin/bash
echo "Begin:"
(sleep 3; echo 3) &
(sleep 5; echo 5) &
(sleep 7; echo 7) &
(sleep 9; echo 9) &
wait %3
echo parent continue
sleep 3
echo end!
[zorro@zorrozou-pc0 bash]$ ./wait.sh
Begin:
3
5
7
parent continue
9
end!

exec

我们已经在重定向那一部分讲过exec处理bash程序的文件描述符的使用方法了,在此补充一下它是如何执行命令的。这个命令的执行过程跟exec族的函数功能是一样的:将当前进程的执行镜像替换成指定进程的执行镜像。还是举例来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[zorro@zorrozou-pc0 bash]$ cat exec.sh
#!/bin/bash
echo "Begin:"
echo "Before exec:"
exec ls /etc/passwd
echo "After exec:"
echo "End!"
[zorro@zorrozou-pc0 bash]$ ./exec.sh
Begin:
Before exec:
/etc/passwd

实际上这个脚本在执行到exec ls /etc/passwd之后,bash进程就已经替换为ls进程了,所以后续的echo命令都不会执行,ls执行完,这个进程就完全退出了。

命令行参数处理

我们已经学习过使用shift方式处理命令行参数了,但是这个功能还是比较简单,它每次执行就仅仅是将参数左移一位而已,将本次的$2变成下次的$1。bash也给我们提供了一个更为专业的命令行参数处理方法,这个命令是getopts。

我们都知道一般的命令参数都是通过-a、-b、-c这样的参数来指定各种功能的,如果我们想要实现这样的功能,只单纯使用shift这样的方式手工处理将会非常麻烦,而且还不能支持让-a -b写成-ab这样的方式。bash跟其他语言一样,提供了getopts这样的方法来帮助我们处理类似的问题,如:

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
[zorro@zorrozou-pc0 bash]$ cat getopts.sh
#!/bin/bash
#getopts的使用方式:字母后面带:的都是需要执行子参数的,如:-c xxxxx -e xxxxxx,后续可以用$OPTARG变量进行判断。
#getopts会将输入的-a -b分别赋值给arg变量,以便后续判断。
while getopts "abc:de:f" arg
do
case $arg in
a)
echo "aaaaaaaaaaaaaaa"
;;
b)
echo "bbbbbbbbbbbbbbb"
;;
c)
echo "c: arg:$OPTARG"
;;
d)
echo "ddddddddddddddd"
;;
e)
echo "e: arg:$OPTARG"
;;
f)
echo "fffffffffffffff"
;;
?)
echo "$arg :no this arguments!"
esac
done

以下为程序输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[zorro@zorrozou-pc0 bash]$ ./getopts.sh -a -bd -c zorro -e jerry
aaaaaaaaaaaaaaa
bbbbbbbbbbbbbbb
ddddddddddddddd
c: arg:zorro
e: arg:jerry
[zorro@zorrozou-pc0 bash]$ ./getopts.sh -c xxxxxxx
c: arg:xxxxxxx
[zorro@zorrozou-pc0 bash]$ ./getopts.sh -a
aaaaaaaaaaaaaaa
[zorro@zorrozou-pc0 bash]$ ./getopts.sh -f
fffffffffffffff
[zorro@zorrozou-pc0 bash]$ ./getopts.sh -g
./getopts.sh: illegal option -- g
unknow argument!

getopts只能处理段格式参数,如:-a这样的。不能支持的是如–login这种长格式参数。实际上我们的系统中还给了一个getopt命令,可以处理长格式参数。这个命令不是内建命令,使用方法跟getopts类似,大家可以自己man getopt近一步学习这个命令的使用,这里就不再赘述了。

进程环境

内建命令中最多的就是关于进程环境的配置的相关命令,当然绝大多数我们之前已经会用了。它们包括:alias、unalias、cd、declare、typeset、dirs、enable、export、hash、history、popd、pushd、local、pwd、readonly、set、unset、shopt、ulimit、umask。

我们在这需要简单说明的命令有:

declare:

typeset:

这两个命令用来声明或显示进程的变量或函数相关信息和属性。如:

declare -a array:可以声明一个数组变量。

declare -A array:可以声明一个关联数组。

declare -f func:可以声明或查看一个函数。

其他常用参数可以help declare查看。

enable:

可以用来打开或者关闭某个内建命令的功能。

dirs:

popd:

pushd:

dirs、popd、pushd可以用来操作目录栈。目录栈是bash提供的一种纪录曾经去过的相关目录的缓存数据结构,可以方便的使操作者在多个深层次的目录中方便的跳转。使用演示:

显示当前目录栈:

1
2
[zorro@zorrozou-pc0 dirstack]$ dirs
~/bash/dirstack

只有一个当前工作目录。将aaa加入目录栈:

1
2
[zorro@zorrozou-pc0 dirstack]$ pushd aaa
~/bash/dirstack/aaa ~/bash/dirstack

pushd除了将目录加入了目录栈外,还改变了当前工作目录。

1
2
[zorro@zorrozou-pc0 aaa]$ pwd
/home/zorro/bash/dirstack/aaa

将bbb目录加入目录栈:

1
2
3
4
5
6
[zorro@zorrozou-pc0 aaa]$ pushd ../bbb/
~/bash/dirstack/bbb ~/bash/dirstack/aaa ~/bash/dirstack
[zorro@zorrozou-pc0 bbb]$ dirs
~/bash/dirstack/bbb ~/bash/dirstack/aaa ~/bash/dirstack
[zorro@zorrozou-pc0 bbb]$ pwd
/home/zorro/bash/dirstack/bbb

加入ccc、ddd、eee目录:

1
2
3
4
5
6
7
8
[zorro@zorrozou-pc0 bbb]$ pushd ../ccc
~/bash/dirstack/ccc ~/bash/dirstack/bbb ~/bash/dirstack/aaa ~/bash/dirstack
[zorro@zorrozou-pc0 ccc]$ pushd ../ddd
~/bash/dirstack/ddd ~/bash/dirstack/ccc ~/bash/dirstack/bbb ~/bash/dirstack/aaa ~/bash/dirstack
[zorro@zorrozou-pc0 ddd]$ pushd ../eee
~/bash/dirstack/eee ~/bash/dirstack/ddd ~/bash/dirstack/ccc ~/bash/dirstack/bbb ~/bash/dirstack/aaa ~/bash/dirstack
[zorro@zorrozou-pc0 eee]$ dirs
~/bash/dirstack/eee ~/bash/dirstack/ddd ~/bash/dirstack/ccc ~/bash/dirstack/bbb ~/bash/dirstack/aaa ~/bash/dirstack

将当前工作目录切换到目录栈中的第2个目录,即当前的ddd目录:

1
2
[zorro@zorrozou-pc0 eee]$ pushd +1
~/bash/dirstack/ddd ~/bash/dirstack/ccc ~/bash/dirstack/bbb ~/bash/dirstack/aaa ~/bash/dirstack ~/bash/dirstack/eee

将当前工作目录切换到目录栈中的第5个目录,即当前的~/bash/dirstack目录:

1
2
[zorro@zorrozou-pc0 ddd]$ pushd +4
~/bash/dirstack ~/bash/dirstack/eee ~/bash/dirstack/ddd ~/bash/dirstack/ccc ~/bash/dirstack/bbb ~/bash/dirstack/aaa

+N表示当前目录栈从左往右数的第N个,第一个是左边的第一个目录,从0开始。
将当前工作目录切换到目录栈中的倒数第3个目录,即当前的ddd目录:

1
2
[zorro@zorrozou-pc0 dirstack]$ pushd -3
~/bash/dirstack/ddd ~/bash/dirstack/ccc ~/bash/dirstack/bbb ~/bash/dirstack/aaa ~/bash/dirstack ~/bash/dirstack/eee

-N表示当亲啊目录栈从右往左数的第N个,第一个是右边的第一个目录,从0开始。
从目录栈中推出一个目录,默认推出当前所在的目录:

1
2
3
4
[zorro@zorrozou-pc0 ccc]$ popd
~/bash/dirstack/ddd ~/bash/dirstack/bbb ~/bash/dirstack/aaa ~/bash/dirstack ~/bash/dirstack/eee
[zorro@zorrozou-pc0 ddd]$ popd
~/bash/dirstack/bbb ~/bash/dirstack/aaa ~/bash/dirstack ~/bash/dirstack/eee

指定要推出的目录编号,数字含义跟pushd一样:

1
2
3
4
5
6
[zorro@zorrozou-pc0 bbb]$ popd +2
~/bash/dirstack/bbb ~/bash/dirstack/aaa ~/bash/dirstack/eee
[zorro@zorrozou-pc0 bbb]$ popd -2
~/bash/dirstack/aaa ~/bash/dirstack/eee
[zorro@zorrozou-pc0 aaa]$ pushd +1
~/bash/dirstack/eee ~/bash/dirstack/aaa

readonly:

声明一个只读变量。

local:

声明一个局部变量。bash的局部变量概念很简单,它只能在函数中使用,并且局部变量只有在函数中可见。

set:

shopt:

我们之前已经讲过这两个命令的使用。这里补充一下其他信息,请参见:http://www.cnblogs.com/ziyunfei/p/4913758.html

eval:

eval是一个可能会被经常用到的内建命令。它的作用其实很简单,就是将指定的命令解析两次。可以这样理解这个命令:

首先我们定义一个变量:

1
2
3
[zorro@zorrozou-pc0 bash]$ pipe="|"
[zorro@zorrozou-pc0 bash]$ echo $pipe
|

这个变量时pipe,值就是”|”这个字符。然后我们试图在后续命令中引入管道这个功能,但是管道符是从变量中引入的,如:

1
2
3
[zorro@zorrozou-pc0 bash]$ cat /etc/passwd $pipe wc -l
cat: invalid option -- 'l'
Try 'cat --help' for more information.

此时执行报错了,因为bash在解释这条命令的时候,并不会先将$pipe解析成”|”再做解释。这时候我们需要让bash先解析$pipe,然后得到”|”字符之后,再将cat /etc/passwd | wc -l当成一个要执行的命令传给bash解释执行。此时我们需要eval:

1
2
[zorro@zorrozou-pc0 bash]$ eval cat /etc/passwd $pipe wc -l
30

这就是eval的用法。再来理解一下,eval就是将所给的命令解析两遍。

最后

通过本文和之前的文章,我们几乎将所有的bash内建命令都覆盖到了。本文主要包括的知识点为:

  1. bash脚本程序的输入输出。
  2. bash的作业控制。
  3. bash脚本的信号处理。
  4. bash对进程的控制。
  5. 命令行参数处理。
  6. 使用内建命令改变bash相关环境。

文章转自SHELL编程之特殊符号

微博ID:orroz

微信公众号:Linux系统技术

前言

本文是shell编程系列的第四篇,集中介绍了bash编程可能涉及到的特殊符号的使用。学会本文内容可以帮助你写出天书一样的bash脚本,并且顺便解决以下问题:

  • 输入输出重定向是什么原理?
  • exec 3<> /tmp/filename是什么鬼?
  • 你玩过bash的关联数组吗?
  • 如何不用if判断变量是否被定义?
  • 脚本中字符串替换和删除操作不用sed怎么做?
  • ” “和’ ‘有什么不同?
  • 正则表达式和bash通配符是一回事么?

这里需要额外注意的是,相同的符号出现在不同的上下文中可能会有不同的含义。我们会在后续的讲解中突出它们的区别。

重定向(REDIRECTION)

重定向也叫输入输出重定向。我们先通过基本的使用对这个概念有个感性认识。

输入重定向

大家应该都用过cat命令,可以输出一个文件的内容。如:cat /etc/passwd。如果不给cat任何参数,那么cat将从键盘(标准输入)读取用户的输入,直接将内容显示到屏幕上,就像这样:

1
2
3
4
5
[zorro@zorrozou-pc0 bash]$ cat
hello
hello
I am zorro!
I am zorro!

可以通过输入重定向让cat命令从别的地方读取输入,显示到当前屏幕上。最简单的方式是输入重定向一个文件,不过这不够“神奇”,我们让cat从别的终端读取输入试试。我当前使用桌面的终端terminal开了多个bash,使用ps命令可以看到这些终端所占用的输入文件是哪个:

1
2
3
4
5
6
7
8
9
10
[zorro@zorrozou-pc0 bash]$ ps ax|grep bash
4632 pts/0 Ss 0:00 -bash
5087 pts/2 S+ 0:00 man bash
5897 pts/1 Ss 0:00 -bash
5911 pts/2 Ss 0:00 -bash
9071 pts/4 Ss 0:00 -bash
11667 pts/3 Ss+ 0:00 -bash
16309 pts/4 S+ 0:00 grep --color=auto bash
19465 pts/2 S 0:00 sudo bash
19466 pts/2 S 0:00 bash

通过第二列可以看到,不同的bash所在的终端文件是哪个,这里的pts/3就意味着这个文件放在/dev/pts/3。我们来试一下,在pts/2对应的bash中输入:

1
[zorro@zorrozou-pc0 bash]$ cat < /dev/pts/3

然后切换到pts/3所在的bash上敲入字符串,在pts/2的bash中能看见相关字符:

1
2
[zorro@zorrozou-pc0 bash]$ cat < /dev/pts/3
safsdfsfsfadsdsasdfsafadsadfd

这只是个输入重定向的例子,一般我们也可以直接cat < /etc/passwd,表示让cat命令不是从默认输入读取,而是从/etc/passwd读取,这就是输入重定向,使用”<“。

输出重定向

绝大多数命令都有输出,用来显示给人看,所以输出基本都显示在屏幕(终端)上。有时候我们不想看到,就可以把输出重定向到别的地方:

[zorro@zorrozou-pc0 bash]$ ls /
bin boot cgroup data dev etc home lib lib64 lost+found mnt opt proc root run sbin srv sys tmp usr var
[zorro@zorrozou-pc0 bash]$ ls / > /tmp/out
[zorro@zorrozou-pc0 bash]$ cat /tmp/out
bin
boot
cgroup
data
dev
……

使用一个”>”,将原本显示在屏幕上的内容给输出到了/tmp/out文件中。这个功能就是输出重定向。

报错重定向

命令执行都会遇到错误,一般也都是给人看的,所以默认还是显示在屏幕上。这些输出使用”>”是不能进行重定向的:

[zorro@zorrozou-pc0 bash]$ ls /1234 > /tmp/err
ls: cannot access ‘/1234’: No such file or directory

可以看到,报错还是显示在了屏幕上。如果想要重定向这样的内容,可以使用”2>”:

[zorro@zorrozou-pc0 bash]$ ls /1234 2> /tmp/err
[zorro@zorrozou-pc0 bash]$ cat /tmp/err
ls: cannot access ‘/1234’: No such file or directory

以上就是常见的输入输出重定向。在进行其它技巧讲解之前,我们有必要理解一下重定向的本质,所以要先从文件描述符说起。

文件描述符(file descriptor)

文件描述符简称fd,它是一个抽象概念,在很多其它体系下,它可能有其它名字,比如在C库编程中可以叫做文件流或文件流指针,在其它语言中也可以叫做文件句柄(handler),而且这些不同名词的隐含意义可能是不完全相同的。不过在系统层,还是应该使用系统调用中规定的名词,我们统一把它叫做文件描述符。

文件描述符本质上是一个数组下标(C语言数组)。在内核中,这个数组是用来管理一个进程打开的文件的对应关系的数组。就是说,对于任何一个进程来说,都有这样一个数组来管理它打开的文件,数组中的每一个元素和文件是映射关系,即:一个数组元素只能映射一个文件,而一个文件可以被多个数组元素所映射。

1
其实上面的描述并不完全准确,在内核中,文件描述符的数组所直接映射的实际上是文件表,文件表再索引到相关文件的v_node。具体可以参见《UNIX系统高级编程》。

shell在产生一个新进程后,新进程的前三个文件描述符都默认指向三个相关文件。这三个文件描述符对应的数组下标分别为0,1,2。0对应的文件叫做标准输入(stdin),1对应的文件叫做标准输出(stdout),2对应的文件叫做标准报错(stderr)。但是实际上,默认跟人交互的输入是键盘、鼠标,输出是显示器屏幕,这些硬件设备对于程序来说都是不认识的,所以操作系统借用了原来“终端”的概念,将键盘鼠标显示器都表现成一个终端文件。于是stdin、stdout和stderr就最重都指向了这所谓的终端文件上。于是,从键盘输入的内容,进程可以从标准输入的0号文件描述符读取,正常的输出内容从1号描述符写出,报错信息被定义为从2号描述符写出。这就是标准输入、标准输出和标准报错对应的描述符编号是0、1、2的原因。这也是为什么对报错进行重定向要使用2>的原因(其实1>也是可以用的)。

明白了以上内容之后,很多重定向的数字魔法就好理解了,比如:

1
2
3
4
5
6
7
[zorro@zorrozou-pc0 prime]$ find /etc -name passwd > /dev/null
find: ‘/etc/docker’: Permission denied
find: ‘/etc/sudoers.d’: Permission denied
find: ‘/etc/lvm/cache’: Permission denied
find: ‘/etc/pacman.d/gnupg/openpgp-revocs.d’: Permission denied
find: ‘/etc/pacman.d/gnupg/private-keys-v1.d’: Permission denied
find: ‘/etc/polkit-1/rules.d’: Permission denied

这相当于只看报错信息。

1
2
3
4
[zorro@zorrozou-pc0 prime]$ find /etc -name passwd 2> /dev/null
/etc/default/passwd
/etc/pam.d/passwd
/etc/passwd

这相当于只看正确输出信息。

1
2
3
4
5
6
7
8
9
10
[zorro@zorrozou-pc0 prime]$ find /etc -name passwd 2>&1
/etc/default/passwd
find: ‘/etc/docker’: Permission denied
/etc/pam.d/passwd
find: ‘/etc/sudoers.d’: Permission denied
find: ‘/etc/lvm/cache’: Permission denied
find: ‘/etc/pacman.d/gnupg/openpgp-revocs.d’: Permission denied
find: ‘/etc/pacman.d/gnupg/private-keys-v1.d’: Permission denied
find: ‘/etc/polkit-1/rules.d’: Permission denied
/etc/passwd

将标准报错输出的,重定向到标准输出再输出。

1
2
3
4
5
6
7
[zorro@zorrozou-pc0 prime]$ echo hello > /tmp/out
[zorro@zorrozou-pc0 prime]$ cat /tmp/out
hello
[zorro@zorrozou-pc0 prime]$ echo hello2 >> /tmp/out
[zorro@zorrozou-pc0 prime]$ cat /tmp/out
hello
hello2

“>>”表示追加重定向。

相信大家对&>>、1>&2、?2>&3、6>&8、>>file 2>&1这样的写法应该也都能理解了。进程可以打开多个文件,多个描述符之间都可以进行重定向。当然,输入也可以,比如:3<表示从描述符3读取。下面我们罗列一下其他重定向符号和用法:

Here Document:

语法:

1
2
3
<<[-]word
here-document
delimiter

这是一种特殊的输入重定向,重定向的内容并不是来自于某个文件,而是从当前输入读取,直到输入中写入了delimiter字符标记结束。用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[zorro@zorrozou-pc0 prime]$ cat << EOF
> hello world!
> I am zorro
>
>
>
> sadfsdf
> ertert
> eof
> EOF
hello world!
I am zorro
sadfsdf
ertert
eof

这个例子可以看到,最后cat输出的内容都是在上面写入的内容,而且内容中不包括EOF,因为EOF是标记输入结束的字符串。这个功能在脚本中通常可以用于需要交互式处理的某些命令的输入和文件编辑,比如想在脚本中使用fdisk命令新建一个分区:

1
2
3
4
5
6
7
8
9
10
[root@zorrozou-pc0 prime]# cat fdisk.sh
#!/bin/bash
fdisk /dev/sdb << EOF
n
p
w
EOF

当然这个脚本大家千万不要乱执行,可能会修改你的分区表。其中要输入的内容,相信熟悉fdisk命令的人应该都能明白,我就不多解释了。

Here strings:

语法:

1
<<<word

使用方式:

1
2
[zorro@zorrozou-pc0 prime]$ cat <<< asdasdasd
asdasdasd

其实就是将<<<符号后面的字符串当成要输入的内容给cat,而不是定向一个文件描述符。这样是不是就相当于把cat当echo用了?

文件描述符的复制:

复制输入文件描述符:[n]<&word

如果n没有指定数字,则默认复制0号文件描述符。word一般写一个已经打开的并且用来作为输入的描述符数字,表示将制订的n号描述符在制定的描述符上复制一个。如果word写的是“-”符号,则表示关闭这个文件描述符。如果word指定的不是一个用来输入的文件描述符,则会报错。

复制输出文件描述符:[n]>&word

复制一个输出的描述符,字段描述参考上面的输入复制,例子上面已经讲过了。这里还需要知道的就是1>&-表示关闭1号描述符。

文件描述符的移动

移动输入描述符:[n]<&digit-

移动输出描述符:[n]>&digit-

这两个符号的意思都是将原有描述符在新的描述符编号上打开,并且关闭原有描述符。

描述符新建:

新建一个用来输入的描述符:[n]<word

新建一个用来输出的描述符:[n]>word

新建一个用来输入和输出的描述符:[n]<>word

word都应该写一个文件路径,用来表示这个文件描述符的关联文件是谁。

下面我们来看相关的编程例子:

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
#!/bin/bash
# example 1
#打开3号fd用来输入,关联文件为/etc/passwd
exec 3< /etc/passwd
#让3号描述符成为标准输入
exec 0<&3
#此时cat的输入将是/etc/passwd,会在屏幕上显示出/etc/passwd的内容。
cat
#关闭3号描述符。
exec 3>&-
# example 2
#打开3号和4号描述符作为输出,并且分别关联文件。
exec 3> /tmp/stdout
exec 4> /tmp/stderr
#将标准输入关联到3号描述符,关闭原来的1号fd。
exec 1>&3-
#将标准报错关联到4号描述符,关闭原来的2号fd。
exec 2>&4-
#这个find命令的所有正常输出都会写到/tmp/stdout文件中,错误输出都会写到/tmp/stderr文件中。
find /etc/ -name "passwd"
#关闭两个描述符。
exec 3>&-
exec 4>&-

以上脚本要注意的地方是,一般输入输出重定向都是放到命令后面作为后缀使用,所以如果单纯改变脚本的描述符,需要在前面加exec命令。这种用法也叫做描述符魔术。某些特殊符号还有一些特殊用法,比如:

1
zorro@zorrozou-pc0 bash]$ > /tmp/out

表示清空文件,当然也可以写成

1
[zorro@zorrozou-pc0 bash]$ :> /tmp/out

因为”:”是一个内建命令,跟true是同样的功能,所以没有任何输出,所以这个命令清空文件的作用。

脚本参数处理

我们在之前的例子中已经简单看过相关参数处理的特殊符号了,再来看一下:

1
2
3
4
5
6
7
8
9
10
11
[zorro@zorrozou-pc0 bash]$ cat arg1.sh
#!/bin/bash
echo $0
echo $1
echo $2
echo $3
echo $4
echo $#
echo $*
echo $?

执行结果:

1
2
3
4
5
6
7
8
9
[zorro@zorrozou-pc0 bash]$ ./arg1.sh 111 222 333 444
./arg1.sh
111
222
333
444
4
111 222 333 444
0

可以罗列一下:

$0:命令名。

$n:n是一个数字,表示第n个参数。

$#:参数个数。

$*:所有参数列表。

$@:同上。

实际上大家可以认为上面的0,1,2,3,#,*,@,?都是一堆变量名。跟aaa=1000定义的变量没什么区别,只是他们有特殊含义。所以$@实际上就是对@变量取值,跟$aaa概念一样。所以上述所有取值都可以写成${}的方式,因为bash中对变量取值有两种写法,另外一种是${aaa}。这种写法的好处是对变量名字可以有更明确的界定,比如:

1
2
3
4
5
6
7
[zorro@zorrozou-pc0 bash]$ aaa=1000
[zorro@zorrozou-pc0 bash]$ echo $aaa
1000
[zorro@zorrozou-pc0 bash]$ echo $aaa0
[zorro@zorrozou-pc0 bash]$ echo ${aaa}0
10000

内建命令shift可以用来对参数进行位置处理,它会将所有参数都左移一个位置,可以用来进行参数处理。使用例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[zorro@zorrozou-pc0 ~]$ cat shift.sh
#!/bin/bash
if [ $# -lt 1 ]
then
echo "Argument num error!" 1>&2
echo "Usage ....." 1>&2
exit
fi
while ! [ -z $1 ]
do
echo $1
shift
done

执行效果:

1
2
3
4
5
6
7
[zorro@zorrozou-pc0 bash]$ ./shift.sh 111 222 333 444 555 666
111
222
333
444
555
666

其他的特殊变量还有:

$?:上一个命令的返回值。

$$:当前shell的PID。

$!:最近一个被放到后台任务管理的进程PID。如:

1
2
3
4
[zorro@zorrozou-pc0 tmp]$ sleep 3000 &
[1] 867
[zorro@zorrozou-pc0 tmp]$ echo $!
867

$-:列出当前bash的运行参数,比如set -v或者-i这样的参数。

$:”“算是所有特殊变量中最诡异的一个了,在bash脚本刚开始的时候,它可以取到脚本的完整文件名。当执行完某个命令之后,它可以取到,这个命令的最后一个参数。当在检查邮件的时候,这个变量帮你保存当前正在查看的邮件名。

数组操作

bash中可以定义数组,使用方法如下:

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
[zorro@zorrozou-pc0 bash]$ cat array.sh
#!/bin/bash
#定义一个一般数组
declare -a array
#为数组元素赋值
array[0]=1000
array[1]=2000
array[2]=3000
array[3]=4000
#直接使用数组名得出第一个元素的值
echo $array
#取数组所有元素的值
echo ${array[*]}
echo ${array[@]}
#取第n个元素的值
echo ${array[0]}
echo ${array[1]}
echo ${array[2]}
echo ${array[3]}
#数组元素个数
echo ${#array[*]}
#取数组所有索引列表
echo ${!array[*]}
echo ${!array[@]}
#定义一个关联数组
declare -A assoc_arr
#为关联数组复制
assoc_arr[zorro]='zorro'
assoc_arr[jerry]='jerry'
assoc_arr[tom]='tom'
#所有操作同上
echo $assoc_arr
echo ${assoc_arr[*]}
echo ${assoc_arr[@]}
echo ${assoc_arr[zorro]}
echo ${assoc_arr[jerry]}
echo ${assoc_arr[tom]}
echo ${#assoc_arr[*]}
echo ${!assoc_arr[*]}
echo ${!assoc_arr[@]}

命令行扩展

大括号扩展

用类似枚举的方式创建一些目录:

1
2
3
[zorro@zorrozou-pc0 bash]$ mkdir -p test/zorro/{a,b,c,d}{1,2,3,4}
[zorro@zorrozou-pc0 bash]$ ls test/zorro/
a1 a2 a3 a4 b1 b2 b3 b4 c1 c2 c3 c4 d1 d2 d3 d4

可能还有这样用的:

1
[zorro@zorrozou-pc0 bash]$ mv test/{a,c}.conf

这个命令的意思是:mv test/a.conf test/c.conf

~符号扩展

~:在bash中一般表示用户的主目录。cd ~表示回到主目录。cd ~zorro表示回到zorro用户的主目录。
变量扩展

我们都知道取一个变量值可以用$或者${}。在使用${}的时候可以添加很多对变量进行扩展操作的功能,下面我们就分别来看看。

${aaa:-1000}

这个表示如果变量aaa是空值或者没有赋值,则此表达式取值为1000,aaa变量不被更改,以后还是空。如果aaa已经被赋值,则原值不变:

1
2
3
4
5
6
7
8
9
10
11
12
[zorro@zorrozou-pc0 bash]$ echo $aaa
[zorro@zorrozou-pc0 bash]$ echo ${aaa:-1000}
1000
[zorro@zorrozou-pc0 bash]$ echo $aaa
[zorro@zorrozou-pc0 bash]$ aaa=2000
[zorro@zorrozou-pc0 bash]$ echo $aaa
2000
[zorro@zorrozou-pc0 bash]$ echo ${aaa:-1000}
2000
[zorro@zorrozou-pc0 bash]$ echo $aaa
2000

${aaa:=1000}

跟上面的表达式的区别是,如果aaa未被赋值,则赋值成=后面的值,其他行为不变:

1
2
3
4
5
6
[zorro@zorrozou-pc0 bash]$ echo $aaa
[zorro@zorrozou-pc0 bash]$ echo ${aaa:=1000}
1000
[zorro@zorrozou-pc0 bash]$ echo $aaa
1000

${aaa:?unset}

判断变量是否未定义或为空,如果符合条件,就提示?后面的字符串。

1
2
3
4
5
[zorro@zorrozou-pc0 bash]$ echo ${aaa:?unset}
-bash: aaa: unset
[zorro@zorrozou-pc0 bash]$ aaa=1000
[zorro@zorrozou-pc0 bash]$ echo ${aaa:?unset}
1000

${aaa:+unset}

如果aaa为空或者未设置,则什么也不做。如果已被设置,则取+后面的值。并不改变原aaa值:

1
2
3
4
5
[zorro@zorrozou-pc0 bash]$ aaa=1000
[zorro@zorrozou-pc0 bash]$ echo ${aaa:+unset}
unset
[zorro@zorrozou-pc0 bash]$ echo $aaa
1000

${aaa:10}

取字符串偏移量,表示取出aaa变量对应字符串的第10个字符之后的字符串,变量原值不变。

1
2
3
[zorro@zorrozou-pc0 bash]$ aaa='/home/zorro/zorro.txt'
[zorro@zorrozou-pc0 bash]$ echo ${aaa:10}
o/zorro.txt

${aaa:10:15}

第二个数字表示取多长:

1
2
[zorro@zorrozou-pc0 bash]$ echo ${aaa:10:5}
o/zor

${!B*}

${!B@}

取出所有以B开头的变量名(请注意他们跟数组中相关符号的差别):

1
2
[zorro@zorrozou-pc0 bash]$ echo ${!B*}
BASH BASHOPTS BASHPID BASH_ALIASES BASH_ARGC BASH_ARGV BASH_CMDS BASH_COMMAND BASH_LINENO BASH_SOURCE BASH_SUBSHELL BASH_VERSINFO BASH_VERSION

${ #aaa }
取变量长度:

1
2
[zorro@zorrozou-pc0 bash]$ echo ${#aaa}
21

${ parameter#word}

变量paramenter看做字符串从左往右找到第一个word,取其后面的字串:

1
2
[zorro@zorrozou-pc0 bash]$ echo ${aaa#/}
home/zorro/zorro.txt

这里需要注意的是,word必须是一个路径匹配的字符串,比如:

1
2
[zorro@zorrozou-pc0 bash]$ echo ${aaa#*zorro}
/zorro.txt

这个表示删除路径中匹配到的第一个zorro左边的所有字符,而这样是无效的:

1
2
[zorro@zorrozou-pc0 bash]$ echo ${aaa#zorro}
/home/zorro/zorro.txt

因为此时zorro不是一个路径匹配。另外,这个表达式只能删除匹配到的左边的字符串,保留右边的。

${ parameter##word}

这个表达式与上一个的区别是,匹配的不是第一个符合条件的word,而是最后一个:

1
2
3
4
[zorro@zorrozou-pc0 bash]$ echo ${aaa##*zorro}
.txt
[zorro@zorrozou-pc0 bash]$ echo ${aaa##*/}
zorro.txt

${ parameter%word}
${ parameter%%word}

这两个符号相对于上面两个相当于#号换成了%号,操作区别也从匹配删除左边的字符变成了匹配删除右边的字符,如:

1
2
3
4
5
6
7
8
[zorro@zorrozou-pc0 bash]$ echo ${aaa%/*}
/home/zorro
[zorro@zorrozou-pc0 bash]$ echo ${aaa%t}
/home/zorro/zorro.tx
[zorro@zorrozou-pc0 bash]$ echo ${aaa%.*}
/home/zorro/zorro
[zorro@zorrozou-pc0 bash]$ echo ${aaa%%z*}
/home/

以上#号和%号分别是匹配删除哪边的,容易记不住。不过有个窍门是,可以看看他们分别在键盘上的$的哪边?在左边的就是匹配删除左边的,在右边就是匹配删除右边的。

${ parameter/pattern/string}

字符串替换,将pattern匹配到的第一个字符串替换成string,pattern可以使用通配符,如:

1
2
3
4
5
6
7
8
[zorro@zorrozou-pc0 bash]$ echo $aaa
/home/zorro/zorro.txt
[zorro@zorrozou-pc0 bash]$ echo ${aaa/zorro/jerry}
/home/jerry/zorro.txt
[zorro@zorrozou-pc0 bash]$ echo ${aaa/zorr?/jerry}
/home/jerry/zorro.txt
[zorro@zorrozou-pc0 bash]$ echo ${aaa/zorr*/jerry}
/home/jerry

${ parameter//pattern/string}

意义同上,不过变成了全局替换:

1
2
[zorro@zorrozou-pc0 bash]$ echo ${aaa//zorro/jerry}
/home/jerry/jerry.txt

${parameter^pattern}
${parameter^^pattern}
${parameter,pattern}
${parameter,,pattern}

1
2
大小写转换,如:

[zorro@zorrozou-pc0 bash]$ echo $aaa
abcdefg
[zorro@zorrozou-pc0 bash]$ echo ${aaa^}
Abcdefg
[zorro@zorrozou-pc0 bash]$ echo ${aaa^^}
ABCDEFG
[zorro@zorrozou-pc0 bash]$ aaa=ABCDEFG
[zorro@zorrozou-pc0 bash]$ echo ${aaa,}
aBCDEFG
[zorro@zorrozou-pc0 bash]$ echo ${aaa,,}
abcdefg

1
2
3
4
5
6
有了以上符号后,很多变量内容的处理就不必再使用sed这样的重型外部命令处理了,可以一定程度的提高bash脚本的执行效率。
### 命令置换
命令置换这个概念就是在命令行中引用一个命令的输出给bash执行,就是我们已经用过的符号,如:

[zorro@zorrozou-pc0 bash]$ echo ls
ls
[zorro@zorrozou-pc0 bash]$ echo ls
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh redirect.sh shift.sh until.sh
alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime select.sh test while.sh

1
2
3
4
5
6
7
8
9
bash会执行放在号中的命令,并将其输出作为bash的命令再执行一遍。在某些情况下双反引号的表达能力有欠缺,比如嵌套的时候就分不清到底是谁嵌套谁?所以bash还提供另一种写法,跟这个符号一样就是$()。
### 算数扩展
$(())
$[]
绝大多数算是表达式可以放在$(())和$[]中进行取值,如:

[zorro@zorrozou-pc0 bash]$ echo $((123+345))
468
[zorro@zorrozou-pc0 bash]$
[zorro@zorrozou-pc0 bash]$
[zorro@zorrozou-pc0 bash]$ echo $((345-123))
222
[zorro@zorrozou-pc0 bash]$ echo $((345*123))
42435
[zorro@zorrozou-pc0 bash]$ echo $((345/123))
2
[zorro@zorrozou-pc0 bash]$ echo $((345%123))
99
[zorro@zorrozou-pc0 bash]$ i=1
[zorro@zorrozou-pc0 bash]$ echo $((i++))
1
[zorro@zorrozou-pc0 bash]$ echo $((i++))
2
[zorro@zorrozou-pc0 bash]$ echo $i
3
[zorro@zorrozou-pc0 bash]$ i=1
[zorro@zorrozou-pc0 bash]$ echo $((++i))
2
[zorro@zorrozou-pc0 bash]$ echo $((++i))
3
[zorro@zorrozou-pc0 bash]$ echo $i
3

1
2
可以支持的运算符包括:

id++ id–

++id –id

    • ! ~
      **
  • / %
    • << >>
      <= >= < >

    == !=
    &
    ^
    |
    &&
    ||
    expr?expr:expr
    = *= /= %= += -= <<= >>= &= ^= |=

    1
    2
    另外可以进行算数运算的还有内建命令let:

[zorro@zorrozou-pc0 bash]$ i=0
[zorro@zorrozou-pc0 bash]$ let ++i
[zorro@zorrozou-pc0 bash]$ echo $i
1
[zorro@zorrozou-pc0 bash]$ i=2
[zorro@zorrozou-pc0 bash]$ let i=i**2
[zorro@zorrozou-pc0 bash]$ echo $i
4

1
2
let的另外一种写法是(()):

[zorro@zorrozou-pc0 bash]$ i=0
[zorro@zorrozou-pc0 bash]$ ((i++))
[zorro@zorrozou-pc0 bash]$ echo $i
1
[zorro@zorrozou-pc0 bash]$ ((i+=4))
[zorro@zorrozou-pc0 bash]$ echo $i
5
[zorro@zorrozou-pc0 bash]$ ((i=i**7))
[zorro@zorrozou-pc0 bash]$ echo $i
78125

1
2
3
4
5
6
### 进程置换
<(list) 和 >(list)
这两个符号可以将list的执行结果当成别的命令需要输入或者输出的文件进行操作,比如我想比较两个命令执行结果的区别:

[zorro@zorrozou-pc0 bash]$ diff <(df -h) <(df)
1,10c1,10
< Filesystem Size Used Avail Use% Mounted on
< dev 7.8G 0 7.8G 0% /dev
< run 7.9G 1.1M 7.8G 1% /run
< /dev/sda3 27G 13G 13G 50% /
< tmpfs 7.9G 500K 7.8G 1% /dev/shm
< tmpfs 7.9G 0 7.9G 0% /sys/fs/cgroup
< tmpfs 7.9G 112K 7.8G 1% /tmp
< /dev/mapper/fedora-home 99G 76G 18G 82% /home
< tmpfs 1.6G 16K 1.6G 1% /run/user/120

< tmpfs 1.6G 16K 1.6G 1% /run/user/1000

Filesystem 1K-blocks Used Available Use% Mounted on
dev 8176372 0 8176372 0% /dev
run 8178968 1052 8177916 1% /run
/dev/sda3 28071076 13202040 13420028 50% /
tmpfs 8178968 500 8178468 1% /dev/shm
tmpfs 8178968 0 8178968 0% /sys/fs/cgroup
tmpfs 8178968 112 8178856 1% /tmp
/dev/mapper/fedora-home 103081248 79381728 18440256 82% /home
tmpfs 1635796 16 1635780 1% /run/user/120
tmpfs 1635796 16 1635780 1% /run/user/1000

1
2
3
4
5
这个符号会将相关命令的输出放到/dev/fd中创建的一个管道文件中,并将管道文件作为参数传递给相关命令进行处理。
### 路径匹配扩展
我们已经知道了路径文件名匹配中的*、?、[abc]这样的符号。bash还给我们提供了一些扩展功能的匹配,需要先使用内建命令shopt打开功能开关。支持的功能有:

?(pattern-list):匹配所给pattern的0次或1次;
*(pattern-list):匹配所给pattern的0次以上包括0次;
+(pattern-list):匹配所给pattern的1次以上包括1次;
@(pattern-list):匹配所给pattern的1次;
!(pattern-list):匹配非括号内的所给pattern。

1
2
使用:

[zorro@zorrozou-pc0 bash]$ shopt -u extglob
[zorro@zorrozou-pc0 bash]$ ls /etc/(a)
/etc/netdata:
apps_groups.conf charts.d.conf netdata.conf

/etc/pcmcia:
config.opts

1
关闭功能之后不能使用:

[zorro@zorrozou-pc0 bash]$ shopt -u extglob
[zorro@zorrozou-pc0 bash]$ ls /etc/(a)
-bash: syntax error near unexpected token `(‘

1
2
3
4
## 其他常用符号
关键字或保留字是一类特殊符号或者单词,它们具有相同的实现属性,即:使用type命令查看其类型都显示key word。

[zorro@zorrozou-pc0 bash]$ type !
! is a shell keyword

1
2
!:当只出现一个叹号的时候代表对表达式(命令的返回值)取非。如:

[zorro@zorrozou-pc0 bash]$ echo hello
hello
[zorro@zorrozou-pc0 bash]$ echo $?
0
[zorro@zorrozou-pc0 bash]$ ! echo hello
hello
[zorro@zorrozou-pc0 bash]$ echo $?
1

1
2
[[]]:这个符号基本跟内建命令test一样,当然我们也知道,内建命令test的另一种写法是[ ]。使用:

[root@zorrozou-pc0 zorro]# [[ -f /etc/passwd ]]
[root@zorrozou-pc0 zorro]# echo $?
0
[root@zorrozou-pc0 zorro]# [[ -f /etc/pass ]]
[root@zorrozou-pc0 zorro]# echo $?
1

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
可以支持的判断参数可以help test查看。
管道”|”或|&:管道其实有两种写法,但是我们一般只常用其中单竖线一种。使用的语法格式:
command1 [ [|⎪|&] command2 ... ]
管道“|”的主要作用是将command1的标准输出跟command2的标准输入通过管道(pipe)连接起来。“|&”这种写法的含义是将command1标准输出和标准报错都跟command2的和准输入连接起来,这相当于是command1 2>&1 | command2的简写方式。
&&:用逻辑与关系连接两个命令,如:command1 && command2,表示当command1执行成功才执行command2,否则command2不会执行。
||:用逻辑或关系连接两个命令,如:command1 || command2,表示当command1执行不成功才执行command2,否则command2不会执行。
有了这两个符号,很多if判断都不用写了。
&:一般作为一个命令或者lists的后缀,表明这个命令的执放到jobs中跑,bash不必wait进程。
;:作为命令或者lists的后缀,主要起到分隔多个命令用的,效果跟回车是一样的。
(list):放在()中执行的命令将在一个subshell环境中执行,这样的命令将打开一个bash子进程执行。即使要执行的是内建命令,也要打开一个subshell的子进程。另外要注意的是,当内建命令前后有管道符号连接的时候,内建命令本身也是要放在subshell中执行的。这个subshell子进程的执行环境基本上是父进程的复制,除了重置了信号的相关设置。bash编程的信号设置使用内建命令trap,将在后续文章中详细说明。
{ list; }:大括号作为函数语法结构中的标记字段和list标记字段,是一个关键字。在大括号中要执行的命令列表(list)会放在当前执行环境中执行。命令列表必须以一个换行或者分号作为标记结束。
## 转义字符
转义字符很重要,所以需要单独拿出来重点说一下。既然bash给我们提供了这么多的特殊字符,那么这些字符对于bash来说就是需要进行特殊处理的。比如我们想创建一个文件名中包含*的文件:

[zorro@zorrozou-pc0 bash]$ ls
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh read.sh select.sh test while.sh
alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime redirect.sh shift.sh until.sh
[zorro@zorrozou-pc0 bash]$ touch *sh

1
这个命令会被bash转义成,对所有文件名以sh结尾的文件做touch操作。那究竟怎么创建这个文件呢?使用转义符:

[zorro@zorrozou-pc0 bash]$ touch *sh
[zorro@zorrozou-pc0 bash]$ ls
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh read.sh select.sh shift.sh until.sh
alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime redirect.sh ‘*sh’ test while.sh

1
2
创建了一个叫做*sh的文件,\就是转义符,它可以转义后面的一个字符。如果我想创建一个名字叫\的文件,就应该:

[zorro@zorrozou-pc0 bash]$ touch \
[zorro@zorrozou-pc0 bash]$ ls
‘\’ alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime redirect.sh ‘*sh’ test while.sh
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh read.sh select.sh shift.sh until.sh

1
如何删除sh呢?rm sh?注意到了么?一不小心就会误操作!正确的做法是:

[zorro@zorrozou-pc0 bash]$ rm *sh
```

可以成功避免这种误操作的习惯是,不要用特殊字符作为文件名或者目录名,不要给自己犯错误的机会!

另外”也是非常重要的转义字符,\只能转义其后面的一个字符,而”可以转义其扩起来的所有字符。另外””也能起到一部分的转义作用,只是它的转义能力没有”强。”和
“”的区别是:”可以转义所有字符,而””不能对$字符、命令置换“和\转义字符进行转义。

最后

先补充一个关于正则表达式的说明:

很多初学者容易将bash的特殊字符和正则表达式搞混,尤其是*、?、[]这些符号。实际上我们要明白,正则表达式跟bash的通配符和特殊符号没有任何关系。bash本身并不支持正则表达式。那些支持正在表达式的都是外部命令,比如grep、sed、awk这些高级文件处理命令。正则表达式是由这些命令自行处理的,而bash并不对正则表达式做任何解析和解释。

关于正则表达式的话题,我们就不在bash编程系列文章中讲解了,不过未来可能会在讲解sed、awk这样的高级文本处理命令中说明。

通过本文我们学习了bash的特殊符号相关内容,主要包括的知识点为:

  1. 输入输出重定向以及描述符魔术。
  2. bash脚本的命令行参数处理。
  3. bash脚本的数组和关联数组。
  4. bash的各种其他扩展特殊字符操作。
  5. 转义字符介绍。
  6. 正则表达式和bash特殊字符的区别。