Linux的内存回收和交换

 

Linux的内存回收和交换

版权声明:

本文章内容在非商业使用前提下可无需授权任意转载、发布。

转载、发布请务必注明作者和其微博、微信公众号地址,以便读者询问问题和甄误反馈,共同进步。

微博ID:orroz

微信公众号:Linux系统技术

前言

Linux的swap相关部分代码从2.6早期版本到现在的4.6版本在细节之处已经有不少变化。本文讨论的swap基于Linux 4.4内核代码。Linux内存管理是一套非常复杂的系统,而swap只是其中一个很小的处理逻辑。希望本文能让读者了解Linux对swap的使用大概是什么样子。阅读完本文,应该可以帮你解决以下问题:

  1. swap到底是干嘛的?
  2. swappiness到底是用来调节什么的?
  3. 什么是内存水位标记?
  4. kswapd什么时候会进行swap操作?
  5. swap分区的优先级(priority)有啥用?

什么是SWAP?

我们一般所说的swap,指的是一个交换分区或文件。在Linux上可以使用swapon -s命令查看当前系统上正在使用的交换空间有哪些,以及相关信息:

[zorro@zorrozou-pc0 linux-4.4]$ swapon -s
Filename Type Size Used Priority
/dev/dm-4 partition 33554428 0 -1

从功能上讲,交换分区主要是在内存不够用的时候,将部分内存上的数据交换到swap空间上,以便让系统不会因内存不够用而导致oom或者更致命的情况出现。所以,当内存使用存在压力,开始触发内存回收的行为时,就可能会使用swap空间。内核对swap的使用实际上是跟内存回收行为紧密结合的。那么内存回收和swap的关系,我们可以提出以下几个问题:

  1. 什么时候会进行内存回收呢?
  2. 哪些内存会可能被回收呢?
  3. 回收的过程中什么时候会进行交换呢?
  4. 具体怎么交换?

下面我们就从这些问题出发,一个一个进行分析。

扫一次只要9毛9,您要不要多扫几次呢?:P

mm_facetoface_collect_qrcode_1465221734716

内存回收

内核之所以要进行内存回收,主要原因有两个:

第一、内核需要为任何时刻突发到来的内存申请提供足够的内存。所以一般情况下保证有足够的free空间对于内核来说是必要的。另外,Linux内核使用cache的策略虽然是不用白不用,内核会使用内存中的page cache对部分文件进行缓存,以便提升文件的读写效率。所以内核有必要设计一个周期性回收内存的机制,以便cache的使用和其他相关内存的使用不至于让系统的剩余内存长期处于很少的状态。

第二,当真的有大于空闲内存的申请到来的时候,会触发强制内存回收。

所以,内核在应对这两类回收的需求下,分别实现了两种不同的机制。一个是使用kswapd进程对内存进行周期检查,以保证平常状态下剩余内存尽可能够用。另一个是直接内存回收(direct page reclaim),就是当内存分配时没有空闲内存可以满足要求时,触发直接内存回收。

这两种内存回收的触发路径不同,一个是由内核进程kswapd直接调用内存回收的逻辑进行内存回收(参见mm/vmscan.c中的kswapd()主逻辑),另一个是内存申请的时候进入slow path的内存申请逻辑进行回收(参见内核代码中的mm/page_alloc.c中的__alloc_pages_slowpath方法)。这两个方法中实际进行内存回收的过程殊途同归,最终都是调用shrink_zone()方法进行针对每个zone的内存页缩减。这个方法中会再调用shrink_lruvec()这个方法对每个组织页的链表进程检查。找到这个线索之后,我们就可以清晰的看到内存回收操作究竟针对的page有哪些了。这些链表主要定义在mm/vmscan.c一个enum中:

#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2

enum lru_list {
LRU_INACTIVE_ANON = LRU_BASE,
LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
LRU_UNEVICTABLE,
NR_LRU_LISTS
};

根据这个enum可以看到,内存回收主要需要进行扫描的包括anon的inactive和active以及file的inactive和active四个链表。就是说,内存回收操作主要针对的就是内存中的文件页(file cache)和匿名页。关于活跃(active)还是不活跃(inactive)的判断内核会使用lru算法进行处理并进行标记,我们这里不详细解释这个过程。

整个扫描的过程分几个循环,首先扫描每个zone上的cgroup组。然后再以cgroup的内存为单元进行page链表的扫描。内核会先扫描anon的active链表,将不频繁的放进inactive链表中,然后扫描inactive链表,将里面活跃的移回active中。进行swap的时候,先对inactive的页进行换出。如果是file的文件映射page页,则判断其是否为脏数据,如果是脏数据就写回,不是脏数据可以直接释放。

这样看来,内存回收这个行为会对两种内存的使用进行回收,一种是anon的匿名页内存,主要回收手段是swap,另一种是file-backed的文件映射页,主要的释放手段是写回和清空。因为针对file based的内存,没必要进行交换,其数据原本就在硬盘上,回收这部分内存只要在有脏数据时写回,并清空内存就可以了,以后有需要再从对应的文件读回来。内存对匿名页和文件缓存一共用了四条链表进行组织,回收过程主要是针对这四条链表进行扫描和操作。

swappiness的作用究竟是什么?

我们应该都知道/proc/sys/vm/swappiness这个文件,是个可以用来调整跟swap相关的参数。这个文件的默认值是60,可以的取值范围是0-100。这很容易给大家一个暗示:我是个百分比哦!那么这个文件具体到底代表什么意思呢?我们先来看一下说明:


swappiness

This control is used to define how aggressive the kernel will swap
memory pages. Higher values will increase agressiveness, lower values
decrease the amount of swap. A value of 0 instructs the kernel not to
initiate swap until the amount of free and file-backed pages is less
than the high water mark in a zone.

The default value is 60.


这个文件的值用来定义内核使用swap的积极程度。值越高,内核就会越积极的使用swap,值越低就会降低对swap的使用积极性。如果这个值为0,那么内存在free和file-backed使用的页面总量小于高水位标记(high water mark)之前,不会发生交换。

在这里我们可以理解file-backed这个词的含义了,实际上就是上文所说的文件映射页的大小。那么这个swappiness到底起到了什么作用呢?我们换个思路考虑这个事情。假设让我们设计一个内存回收机制,要去考虑将一部分内存写到swap分区上,将一部分file-backed的内存写回并清空,剩余部分内存出来,我们将怎么设计?

我想应该主要考虑这样几个问题。

  1. 如果回收内存可以有两种途径(匿名页交换和file缓存清空),那么我应该考虑在本次回收的时候,什么情况下多进行file写回,什么情况下应该多进行swap交换。说白了就是平衡两种回收手段的使用,以达到最优。
  2. 如果符合交换条件的内存较长,是不是可以不用全部交换出去?比如可以交换的内存有100M,但是目前只需要50M内存,实际只要交换50M就可以了,不用把能交换的都交换出去。

分析代码会发现,Linux内核对这部分逻辑的实现代码在get_scan_count()这个方法中,这个方法被shrink_lruvec()调用。get_sacn_count()就是处理上述逻辑的,swappiness是它所需要的一个参数,这个参数实际上是指导内核在清空内存的时候,是更倾向于清空file-backed内存还是更倾向于进行匿名页的交换的。当然,这只是个倾向性,是指在两个都够用的情况下,更愿意用哪个,如果不够用了,那么该交换还是要交换。

简单看一下get_sacn_count()函数的处理部分代码,其中关于swappiness的第一个处理是:

 /*
* With swappiness at 100, anonymous and file have the same priority.
* This scanning priority is essentially the inverse of IO cost.
*/
anon_prio = swappiness;
file_prio = 200 - anon_prio;

这里注释的很清楚,如果swappiness设置为100,那么匿名页和文件将用同样的优先级进行回收。很明显,使用清空文件的方式将有利于减轻内存回收时可能造成的IO压力。因为如果file-backed中的数据不是脏数据的话,那么可以不用写回,这样就没有IO发生,而一旦进行交换,就一定会造成IO。所以系统默认将swappiness的值设置为60,这样回收内存时,对file-backed的文件cache内存的清空比例会更大,内核将会更倾向于进行缓存清空而不是交换。

这里的swappiness值如果是60,那么是不是说内核回收的时候,会按照60:140的比例去做相应的swap和清空file-backed的空间呢?并不是。在做这个比例计算的时候,内核还要参考当前内存使用的其他信息。对这里具体是怎么处理感兴趣的人,可以自己详细看get_sacn_count()的实现,本文就不多解释了。我们在此要明确的概念是:swappiness的值是用来控制内存回收时,回收的匿名页更多一些还是回收的file cache更多一些

那么swappiness设置为0的话,是不是内核就根本不会进行swap了呢?这个答案也是否定的。首先是内存真的不够用的时候,该swap的话还是要swap。其次在内核中还有一个逻辑会导致直接使用swap,内核代码是这样处理的:

 /*
* Prevent the reclaimer from falling into the cache trap: as
* cache pages start out inactive, every cache fault will tip
* the scan balance towards the file LRU. And as the file LRU
* shrinks, so does the window for rotation from references.
* This means we have a runaway feedback loop where a tiny
* thrashing file LRU becomes infinitely more attractive than
* anon pages. Try to detect this based on file LRU size.
*/
if (global_reclaim(sc)) {
unsigned long zonefile;
unsigned long zonefree;

zonefree = zone_page_state(zone, NR_FREE_PAGES);
zonefile = zone_page_state(zone, NR_ACTIVE_FILE) +
zone_page_state(zone, NR_INACTIVE_FILE);

if (unlikely(zonefile + zonefree <= high_wmark_pages(zone))) {
scan_balance = SCAN_ANON;
goto out;
}
}

这里的逻辑是说,如果触发的是全局回收,并且zonefile + zonefree <= high_wmark_pages(zone)条件成立时,就将scan_balance这个标记置为SCAN_ANON。后续处理scan_balance的时候,如果它的值是SCAN_ANON,则一定会进行针对匿名页的swap操作。要理解这个行为,我们首先要搞清楚什么是高水位标记(high_wmark_pages)。

扫一次只要9毛9,欢迎您多扫几次!

mm_facetoface_collect_qrcode_1465221734716

内存水位标记(watermark)

我们回到kswapd周期检查和直接内存回收的两种内存回收机制。直接内存回收比较好理解,当申请的内存大于剩余内存的时候,就会触发直接回收。那么kswapd进程在周期检查的时候触发回收的条件是什么呢?还是从设计角度来看,kswapd进程要周期对内存进行检测,达到一定阈值的时候开始进行内存回收。这个所谓的阈值可以理解为内存目前的使用压力,就是说,虽然我们还有剩余内存,但是当剩余内存比较小的时候,就是内存压力较大的时候,就应该开始试图回收些内存了,这样才能保证系统尽可能的有足够的内存给突发的内存申请所使用。

那么如何描述内存使用的压力呢?Linux内核使用水位标记(watermark)的概念来描述这个压力情况。Linux为内存的使用设置了三种内存水位标记,high、low、min。他们所标记的分别含义为:剩余内存在high以上表示内存剩余较多,目前内存使用压力不大;high-low的范围表示目前剩余内存存在一定压力;low-min表示内存开始有较大使用压力,剩余内存不多了;min是最小的水位标记,当剩余内存达到这个状态时,就说明内存面临很大压力。小于min这部分内存,内核是保留给特定情况下使用的,一般不会分配。内存回收行为就是基于剩余内存的水位标记进行决策的,当系统剩余内存低于watermark[low]的时候,内核的kswapd开始起作用,进行内存回收。直到剩余内存达到watermark[high]的时候停止。如果内存消耗导致剩余内存达到了或超过了watermark[min]时,就会触发直接回收(direct reclaim)。

明白了水位标记的概念之后,zonefile + zonefree <= high_wmark_pages(zone)这个公式就能理解了。这里的zonefile相当于内存中文件映射的总量,zonefree相当于剩余内存的总量。内核一般认为,如果zonefile还有的话,就可以尽量通过清空文件缓存获得部分内存,而不必只使用swap方式对anon的内存进行交换。整个判断的概念是说,在全局回收的状态下(有global_reclaim(sc)标记),如果当前的文件映射内存总量+剩余内存总量的值评估小于等于watermark[high]标记的时候,就可以进行直接swap了。这样是为了防止进入cache陷阱,具体描述可以见代码注释。这个判断对系统的影响是,swappiness设置为0时,有剩余内存的情况下也可能发生交换。

那么watermark相关值是如何计算的呢?所有的内存watermark标记都是根据当前内存总大小和一个可调参数进行运算得来的,这个参数是:/proc/sys/vm/min_free_kbytes。首先这个参数本身决定了系统中每个zone的watermark[min]的值大小,然后内核根据min的大小并参考每个zone的内存大小分别算出每个zone的low水位和high水位值。
想了解具体逻辑可以参见源代码目录下的:mm/page_alloc.c文件。在系统中可以从/proc/zoneinfo文件中查看当前系统的相关的信息和使用情况。

我们会发现以上内存管理的相关逻辑都是以zone为单位的,这里zone的含义是指内存的分区管理。Linux将内存分成多个区,主要有直接访问区(DMA)、一般区(Normal)和高端内存区(HighMemory)。内核对内存不同区域的访问因为硬件结构因素会有寻址和效率上的差别。如果在NUMA架构上,不同CPU所管理的内存也是不同的zone。

相关参数设置

zone_reclaim_mode

zone_reclaim_mode模式是在2.6版本后期开始加入内核的一种模式,可以用来管理当一个内存区域(zone)内部的内存耗尽时,是从其内部进行内存回收还是可以从其他zone进行回收的选项,我们可以通过/proc/sys/vm/zone_reclaim_mode文件对这个参数进行调整。

在申请内存时(内核的get_page_from_freelist()方法中),内核在当前zone内没有足够内存可用的情况下,会根据zone_reclaim_mode的设置来决策是从下一个zone找空闲内存还是在zone内部进行回收。这个值为0时表示可以从下一个zone找可用内存,非0表示在本地回收。这个文件可以设置的值及其含义如下:

  1. echo 0 > /proc/sys/vm/zone_reclaim_mode:意味着关闭zone_reclaim模式,可以从其他zone或NUMA节点回收内存。
  2. echo 1 > /proc/sys/vm/zone_reclaim_mode:表示打开zone_reclaim模式,这样内存回收只会发生在本地节点内。
  3. echo 2 > /proc/sys/vm/zone_reclaim_mode:在本地回收内存时,可以将cache中的脏数据写回硬盘,以回收内存。
  4. echo 4 > /proc/sys/vm/zone_reclaim_mode:可以用swap方式回收内存。

不同的参数配置会在NUMA环境中对其他内存节点的内存使用产生不同的影响,大家可以根据自己的情况进行设置以优化你的应用。默认情况下,zone_reclaim模式是关闭的。这在很多应用场景下可以提高效率,比如文件服务器,或者依赖内存中cache比较多的应用场景。这样的场景对内存cache速度的依赖要高于进程进程本身对内存速度的依赖,所以我们宁可让内存从其他zone申请使用,也不愿意清本地cache。

如果确定应用场景是内存需求大于缓存,而且尽量要避免内存访问跨越NUMA节点造成的性能下降的话,则可以打开zone_reclaim模式。此时页分配器会优先回收容易回收的可回收内存(主要是当前不用的page cache页),然后再回收其他内存。

打开本地回收模式的写回可能会引发其他内存节点上的大量的脏数据写回处理。如果一个内存zone已经满了,那么脏数据的写回也会导致进程处理速度收到影响,产生处理瓶颈。这会降低某个内存节点相关的进程的性能,因为进程不再能够使用其他节点上的内存。但是会增加节点之间的隔离性,其他节点的相关进程运行将不会因为另一个节点上的内存回收导致性能下降。

除非针对本地节点的内存限制策略或者cpuset配置有变化,对swap的限制会有效约束交换只发生在本地内存节点所管理的区域上。

min_unmapped_ratio

这个参数只在NUMA架构的内核上生效。这个值表示NUMA上每个内存区域的pages总数的百分比。在zone_reclaim_mode模式下,只有当相关区域的内存使用达到这个百分比,才会发生区域内存回收。在zone_reclaim_mode设置为4的时候,内核会比较所有的file-backed和匿名映射页,包括swapcache占用的页以及tmpfs文件的总内存使用是否超过这个百分比。其他设置的情况下,只比较基于一般文件的未映射页,不考虑其他相关页。

page-cluster

page-cluster是用来控制从swap空间换入数据的时候,一次连续读取的页数,这相当于对交换空间的预读。这里的连续是指在swap空间上的连续,而不是在内存地址上的连续。因为swap空间一般是在硬盘上,对硬盘设备的连续读取将减少磁头的寻址,提高读取效率。这个文件中设置的值是2的指数。就是说,如果设置为0,预读的swap页数是2的0次方,等于1页。如果设置为3,就是2的3次方,等于8页。同时,设置为0也意味着关闭预读功能。文件默认值为3。我们可以根据我们的系统负载状态来设置预读的页数大小。

swap的相关操纵命令

可以使用mkswap将一个分区或者文件创建成swap空间。swapon可以查看当前的swap空间和启用一个swap分区或者文件。swapoff可以关闭swap空间。我们使用一个文件的例子来演示一下整个操作过程:

制作swap文件:

[root@zorrozou-pc0 ~]# dd if=/dev/zero of=./swapfile bs=1M count=8G
dd: error writing './swapfile': No space left on device
14062+0 records in
14061+0 records out
14744477696 bytes (15 GB, 14 GiB) copied, 44.0824 s, 334 MB/s
[root@zorrozou-pc0 ~]# mkswap swapfile 
mkswap: swapfile: insecure permissions 0644, 0600 suggested.
Setting up swapspace version 1, size = 13.7 GiB (14744473600 bytes)
no label, UUID=a0ac2a67-0f68-4189-939f-4801bec7e8e1

启用swap文件:

[root@zorrozou-pc0 ~]# swapon swapfile 
swapon: /root/swapfile: insecure permissions 0644, 0600 suggested.
[root@zorrozou-pc0 ~]# swapon -s
Filename Type Size Used Priority
/dev/dm-4 partition 33554428 9116 -1
/root/swapfile file 14398900 0 -2

关闭swap空间:

[root@zorrozou-pc0 ~]# swapoff /root/swapfile 
[root@zorrozou-pc0 ~]# swapon -s
Filename Type Size Used Priority
/dev/dm-4 partition 33554428 9116 -1

在使用多个swap分区或者文件的时候,还有一个优先级的概念(Priority)。在swapon的时候,我们可以使用-p参数指定相关swap空间的优先级,值越大优先级越高,可以指定的数字范围是-1到32767。内核在使用swap空间的时候总是先使用优先级高的空间,后使用优先级低的。当然如果把多个swap空间的优先级设置成一样的,那么两个swap空间将会以轮询方式并行进行使用。如果两个swap放在两个不同的硬盘上,相同的优先级可以起到类似RAID0的效果,增大swap的读写效率。另外,编程时使用mlock()也可以将指定的内存标记为不会换出,具体帮助可以参考man 2 mlock。

最后

关于swap的使用建议,针对不同负载状态的系统是不一样的。有时我们希望swap大一些,可以在内存不够用的时候不至于触发oom-killer导致某些关键进程被杀掉,比如数据库业务。也有时候我们希望不要swap,因为当大量进程爆发增长导致内存爆掉之后,会因为swap导致IO跑死,整个系统都卡住,无法登录,无法处理。这时候我们就希望不要swap,即使出现oom-killer也造成不了太大影响,但是不能允许服务器因为IO卡死像多米诺骨牌一样全部死机,而且无法登陆。跑cpu运算的无状态的apache就是类似这样的进程池架构的程序。

所以,swap到底怎么用?要还是不要?设置大还是小?相关参数应该如何配置?是要根据我们自己的生产环境的情况而定的。阅读完本文后希望大家可以明白一些swap的深层次知识。我简单总结一下:

  1. 一个内存剩余还比较大的系统中,是否有可能使用swap?有可能,如果运行中的某个阶段出发了这个条件:zonefile + zonefree <= high_wmark_pages(zone),就可能会swap。
  2. swappiness设置为0就相当于关闭swap么?不是的,关闭swap要使用swapoff命令。swappiness只是在内存发生回收操作的时候用来平衡cache回收和swap交换的一个参数,调整为0意味着,尽量通过清缓存来回收内存。
  3. swappiness设置为100代表系统会尽量少用剩余内存而多使用swap么?不是的,这个值设置为100表示内存发生回收时,从cache回收内存和swap交换的优先级一样。就是说,如果目前需求100M内存,那么较大机率会从cache中清除50M内存,再将匿名页换出50M,把回收到的内存给应用程序使用。但是这还要看cache中是否能有空间,以及swap是否可以交换50m。内核只是试图对它们平衡一些而已。
  4. kswapd进程什么时候开始内存回收?kswapd根据内存水位标记决定是否开始回收内存,如果标记达到low就开始回收,回收到剩余内存达到high标记为止。
  5. 如何查看当前系统的内存水位标记?cat /proc/zoneinfo。

如果对本文有相关问题,可以在我的微博、微信或者博客上联系我。

扫一次只要9毛9,您扫了几次呢?

mm_facetoface_collect_qrcode_1465221734716


大家好,我是Zorro!

如果你喜欢本文,欢迎在微博上搜索“orroz”关注我,地址是:http://weibo.com/orroz

大家也可以在微信上搜索:Linux系统技术 关注我的公众号。

我的所有文章都会沉淀在我的个人博客上,地址是:http://liwei.life。

欢迎使用以上各种方式一起探讨学习,共同进步。

公众号二维码:

Zorro] icon


 

SHELL编程之常用技巧

 

SHELL编程之常用技巧

版权声明:

本文章内容在非商业使用前提下可无需授权任意转载、发布。

转载、发布请务必注明作者和其微博、微信公众号地址,以便读者询问问题和甄误反馈,共同进步。

微博ID:orroz

微信公众号:Linux系统技术

前言

本文是shell编程系列的第六篇,集中介绍了bash编程中部分高级编程方法和技巧。通过学习本文内容,可以帮你解决以下问题:

  1. bash可以网络编程么?
  2. .(){ .|.& };. 据说执行这些符号可以死机,那么它们是啥意思?
  3. 你是什么保证crond中的任务不重复执行的?grep一下然后wc算一下个数么?
  4. 受限模式执行bash可以保护什么?
  5. 啥时候会出现subshell?
  6. coproc协进程怎么用?

请刷下面二维码捐赠0.99元。穷佐罗将持续为您奉上高质量的技术文章。多谢支持!

mm_facetoface_collect_qrcode_1465221734716

/dev和/proc目录

dev目录是系统中集中用来存放设备文件的目录。除了设备文件以外,系统中也有不少特殊的功能通过设备的形式表现出来。设备文件是一种特殊的文件,它们实际上是驱动程序的接口。在Linux操作系统中,很多设备都是通过设备文件的方式为进程提供了输入、输出的调用标准,这也符合UNIX的“一切皆文件”的设计原则。所以,对于设备文件来说,文件名和路径其实都不重要,最重要的使其主设备号和辅助设备号,就是用ls -l命令显示出来的原本应该出现在文件大小位置上的两个数字,比如下面命令显示的8和0:

[zorro@zorrozou-pc0 bash]$ ls -l /dev/sda
brw-rw---- 1 root disk 8, 0 5月 12 10:47 /dev/sda

设备文件的主设备号对应了这种设备所使用的驱动是哪个,而辅助设备号则表示使用同一种驱动的设备编号。我们可以使用mknod命令手动创建一个设备文件:

[zorro@zorrozou-pc0 bash]$ sudo mknod harddisk b 8 0 
[zorro@zorrozou-pc0 bash]$ ls -l harddisk 
brw-r--r-- 1 root root 8, 0 5月 18 09:49 harddisk

这样我们就创建了一个设备文件叫harddisk,实际上它跟/dev/sda是同一个设备,因为它们对应的设备驱动和编号都一样。所以这个设备实际上是跟sda相同功能的设备。

系统还给我们提供了几个有特殊功能的设备文件,在bash编程的时候可能会经常用到:

/dev/null:黑洞文件。可以对它重定向如何输出。

/dev/zero:0发生器。可以产生二进制的0,产生多少根使用时间长度有关。我们经常用这个文件来产生大文件进行某些测试,如:

[zorro@zorrozou-pc0 bash]$ dd if=/dev/zero of=./bigfile bs=1M count=1024
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.3501 s, 3.1 GB/s

dd命令也是我们在bash编程中可能会经常使用到的命令。

/dev/random:Linux下的random文件是一个根据计算机背景噪声而产生随机数的真随机数发生器。所以,如果容纳噪声数据的熵池空了,那么对文件的读取会出现阻塞。

/dev/urandom:是一个伪随机数发生器。实际上在Linux的视线中,urandom产生随机数的方法根random一样,只是它可以重复使用熵池中的数据。这两个文件在不同的类unix系统中可能实现方法不同,请注意它们的区别。

/dev/tcp & /dev/udp:这两个神奇的目录为bash编程提供了一种可以进行网络编程的功能。在bash程序中使用/dev/tcp/ip/port的方式就可以创建一个scoket作为客户端去连接服务端的ip:port。我们用一个检查http协议的80端口是否打开的例子来说明它的使用方法:

[zorro@zorrozou-pc0 bash]$ cat tcp.sh
#!/bin/bash

ipaddr=127.0.0.1
port=80

if ! exec 5<> /dev/tcp/$ipaddr/$port
then
exit 1
fi

echo -e "GET / HTTP/1.0\n" >&5

cat <&5

ipaddr的部分还可以写一个主机名。大家可以用此脚本分别在本机打开web服务和不打开的情况下分别执行观察是什么效果。

/proc是另一个我们经常使用的目录。这个目录完全是内核虚拟的。内核将一些系统信息都放在/proc目录下一文件和文本的方式显示出来,如:/proc/cpuinfo、/proc/meminfo。我们可以使用man 5 proc来查询这个目录下文件的作用。

函数和递归

我们已经接触过函数的概念了,在bash编程中,函数无非是将一串命令起了个名字,后续想要调用这一串命令就可以直接写函数的名字了。在语法上定义一个函数的方法是:

name () compound-command [redirection]
function name [()] compound-command [redirection]

我们可以加function关键字显式的定义一个函数,也可以不加。函数在定义的时候可以直接在后面加上重定向的处理。这里还需要特殊说明的是函数的参数处理和局部变量,请看下面脚本:

[zorro@zorrozou-pc0 bash]$ cat function.sh |awk '{print "\t"$0}'
#!/bin/bash

aaa=1000

arg_proc () {
echo "Function begin:"
local aaa=2000
echo $1
echo $2
echo $3
echo $*
echo $@
echo $aaa
echo "Function end!"
}

echo "Script bugin:"
echo $1
echo $2
echo $3
echo $*
echo $@
echo $aaa

arg_proc aaa bbb ccc ddd eee fff

echo $1
echo $2
echo $3
echo $*
echo $@
echo $aaa
echo "Script end!"

我们带-x参数执行一下:

+ aaa=1000
+ echo 'Script bugin:'
Script bugin:
+ echo 111
111
+ echo 222
222
+ echo 333
333
+ echo 111 222 333 444 555
111 222 333 444 555
+ echo 111 222 333 444 555
111 222 333 444 555
+ echo 1000
1000
+ arg_proc aaa bbb ccc ddd eee fff
+ echo 'Function begin:'
Function begin:
+ local aaa=2000
+ echo aaa
aaa
+ echo bbb
bbb
+ echo ccc
ccc
+ echo aaa bbb ccc ddd eee fff
aaa bbb ccc ddd eee fff
+ echo aaa bbb ccc ddd eee fff
aaa bbb ccc ddd eee fff
+ echo 2000
2000
+ echo 'Function end!'
Function end!
+ echo 111
111
+ echo 222
222
+ echo 333
333
+ echo 111 222 333 444 555
111 222 333 444 555
+ echo 111 222 333 444 555
111 222 333 444 555
+ echo 1000
1000
+ echo 'Script end!'
Script end!

观察整个执行过程可以发现,函数的参数适用方法跟脚本一样,都可以使用$n、$*、$@这些符号来处理。而且函数参数跟函数内部使用local定义的局部变量效果一样,都是只在函数内部能看到。函数外部看不到函数里定义的局部变量,当函数内部的局部变量和外部的全局变量名字相同时,函数内只能取到局部变量的值。当函数内部没有定义跟外部同名的局部变量的时候,函数内部也可以看到全局变量。

bash编程支持递归调用函数,跟其他编程语言不同的地方是,bash还可以递归的调用自身,这在某些编程场景下非常有用。我们先来看一个递归的简单例子:

[zorro@zorrozou-pc0 bash]$ cat recurse.sh
#!/bin/bash

read_dir () {
for i in $1/*
do
if [ -d $i ]
then
read_dir $i
else
echo $i
fi
done

}

read_dir $1

这个脚本可以遍历一个目录下所有子目录中的非目录文件。关于递归,还有一个经典的例子,fork炸弹:

.(){ .|.& };.

这一堆符号看上去很令人费解,我们来解释一下每个符号的含义:根据函数的定义语法,我们知道.(){}的意思是,定义一个函数名子叫“.”。虽然系统中又个内建命令也叫.,就是source命令,但是我们也知道,当函数和内建命令名字冲突的时候,bash首先会将名字当成是函数来解释。在{}包含的函数体中,使用了一个管道连接了两个点,这里的第一个.就是函数的递归调用,我们也知道了使用管道的时候会打开一个subshell的子进程,所以在这里面就递归的打开了子进程。{}后面的分号只表示函数定义完毕的结束符,在之后就是调用函数名执行的.,之后函数开始递归的打开自己,去产生子进程,直到系统崩溃为止。

请刷下面二维码捐赠0.99元。穷佐罗将持续为您奉上高质量的技术文章。多谢支持!

mm_facetoface_collect_qrcode_1465221734716

bash并发编程和flock

在shell编程中,需要使用并发编程的场景并不多。我们倒是经常会想要某个脚本不要同时出现多次同时执行,比如放在crond中的某个周期任务,如果执行时间较长以至于下次再调度的时间间隔,那么上一个还没执行完就可能又打开一个,这时我们会希望本次不用执行。本质上讲,无论是只保证任何时候系统中只出现一个进程还是多个进程并发,我们需要对进程进行类似的控制。因为并发的时候也会有可能产生竞争条件,导致程序出问题。

我们先来看如何写一个并发的bash程序。在前文讲到作业控制和wait命令使用的时候,我们就已经写了一个简单的并发程序了,我们这次让它变得复杂一点。我们写一个bash脚本,创建一个计数文件,并将里面的值写为0。然后打开100个子进程,每个进程都去读取这个计数文件的当前值,并加1写回去。如果程序执行正确,最后里面的值应该是100,因为每个子进程都会累加一个1写入文件,我们来试试:

[zorro@zorrozou-pc0 bash]$ cat racing.sh
#!/bin/bash

countfile=/tmp/count

if ! [ -f $countfile ]
then
echo 0 > $countfile
fi

do_count () {
read count < $countfile
echo $((++count)) > $countfile
}

for i in `seq 1 100`
do
do_count &
done

wait

cat $countfile

rm $countfile

我们再来看看这个程序的执行结果:

[zorro@zorrozou-pc0 bash]$ ./racing.sh 
26
[zorro@zorrozou-pc0 bash]$ ./racing.sh 
13
[zorro@zorrozou-pc0 bash]$ ./racing.sh 
34
[zorro@zorrozou-pc0 bash]$ ./racing.sh 
25
[zorro@zorrozou-pc0 bash]$ ./racing.sh 
45
[zorro@zorrozou-pc0 bash]$ ./racing.sh 
5

多次执行之后,每次得到的结果都不一样,也没有一次是正确的结果。这就是典型的竞争条件引起的问题。当多个进程并发的时候,如果使用的共享的资源,就有可能会造成这样的问题。这里的竞争调教就是:当某一个进程读出文件值为0,并加1,还没写回去的时候,如果有别的进程读了文件,读到的还是0。于是多个进程会写1,以及其它的数字。解决共享文件的竞争问题的办法是使用文件锁。每个子进程在读取文件之前先给文件加锁,写入之后解锁,这样临界区代码就可以互斥执行了:

[zorro@zorrozou-pc0 bash]$ cat flock.sh
#!/bin/bash

countfile=/tmp/count

if ! [ -f $countfile ]
then
echo 0 > $countfile
fi

do_count () {
exec 3< $countfile
#对三号描述符加互斥锁
flock -x 3
read -u 3 count
echo $((++count)) > $countfile
#解锁
flock -u 3
#关闭描述符也会解锁
exec 3>&-
}

for i in `seq 1 100`
do
do_count &
done

wait

cat $countfile

rm $countfile
[zorro@zorrozou-pc0 bash]$ ./flock.sh 
100

对临界区代码进行加锁处理之后,程序执行结果正确了。仔细思考一下程序之后就会发现,这里所谓的临界区代码由加锁前的并行,变成了加锁后的串行。flock的默认行为是,如果文件之前没被加锁,则加锁成功返回,如果已经有人持有锁,则加锁行为会阻塞,直到成功加锁。所以,我们也可以利用互斥锁的这个特征,让bash脚本不会重复执行。

[zorro@zorrozou-pc0 bash]$ cat repeat.sh
#!/bin/bash

exec 3> /tmp/.lock

if ! flock -xn 3
then
echo "already running!"
exit 1
fi

echo "running!"
sleep 30
echo "ending"

flock -u 3
exec 3>&-
rm /tmp/.lock

exit 0

-n参数可以让flock命令以非阻塞方式探测一个文件是否已经被加锁,所以可以使用互斥锁的特点保证脚本运行的唯一性。脚本退出的时候锁会被释放,所以这里可以不用显式的使用flock解锁。flock除了-u参数指定文件描述符锁文件以外,还可以作为执行命令的前缀使用。这种方式非常适合直接在crond中方式所要执行的脚本重复执行。如:

*/1 * * * * /usr/bin/flock -xn /tmp/script.lock -c '/home/bash/script.sh'

关于flock的其它参数,可以man flock找到说明。

受限bash

以受限模式执行bash程序,有时候是很有必要的。这种模式可以保护我们的很多系统环境不受bash程序的误操作影响。启动受限模式的bash的方法是使用-r参数,或者也可以rbash的进程名方式执行bash。受限模式的bash和正常bash时间的差别是:

  1. 不能使用cd命令改变当前工作目录。
  2. 不能改变SHELL、PATH、ENV和BASH_ENV环境变量。
  3. 不能调用含有/的命令路径。
  4. 不能使用.执行带有/字符的命令路径。
  5. 不能使用hash命令的-p参数指定一个带斜杠\的参数。
  6. 不能在shell环境启动的时候加载函数的定义。
  7. 不能检查SHELLOPTS变量的内容。
  8. 不能使用>, >|, <>, >&, &>和 >>重定向操作符。
  9. 不能使用exec命令使用一个新程序替换当前执行的bash进程。
  10. enable内建命令不能使用-f、-d参数。
  11. 不可以使用enable命令打开或者关闭内建命令。
  12. command命令不可以使用-p参数。
  13. 不能使用set +r或者set +o restricted命令关闭受限模式。

测试一个简单的受限模式:

[zorro@zorrozou-pc0 bash]$ cat restricted.sh 
#!/bin/bash

set -r

cd /tmp
[zorro@zorrozou-pc0 bash]$ ./restricted.sh 
./restricted.sh: line 5: cd: restricted

subshell

我们前面接触过subshell的概念,我们之前说的是,当一个命令放在()中的时候,bash会打开一个子进程去执行相关命令,这个子进程实际上是另一个bash环境,叫做subshell。当然包括放在()中执行的命令,bash会在以下情况下打开一个subshell执行命令:

  1. 使用&作为命令结束提交了作业控制任务时。
  2. 使用|连接的命令会在subshell中打开。
  3. 使用()封装的命令。
  4. 使用coproc(bash 4.0版本之后支持)作为前缀执行的命令。
  5. 要执行的文件不存在或者文件存在但不具备可执行权限的时候,这个执行过程会打开一个subshell执行。

在subshell中,有些事情需要注意。subshell中的$$取到的仍然是父进程bash的pid,如果想要取到subshell的pid,可以使用BASHPID变量:

[zorro@zorrozou-pc0 bash]$ echo $$ ;echo $BASHPID && (echo $$;echo $BASHPID)
5484
5484
5484
24584

可以使用BASH_SUBSHELL变量的值来检查当前环境是不是在subshell中,这个值在非subshell中是0;每进入一层subshell就加1。

[zorro@zorrozou-pc0 bash]$ echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL))
0
1
2

在subshell中做的任何操作都不会影响父进程的bash执行环境。subshell除了PID和trap相关设置外,其他的环境都跟父进程是一样的。subshell的trap设置跟父进程刚启动的时候还没做trap设置之前一样。

协进程coprocess

在bash 4.0版本之后,为我们提供了一个coproc关键字可以支持协进程。协进程提供了一种可以上bash移步执行另一个进程的工作模式,实际上跟作业控制类似。严格来说,bash的协进程就是使用作业控制作为实现手段来做的。它跟作业控制的区别仅仅在于,协进程的标准输入和标准输出都在调用协进程的bash中可以取到文件描述符,而作业控制进程的标准输入和输出都是直接指向终端的。我们来看看使用协进程的语法:

coproc [NAME] command [redirections]

使用coproc作为前缀,后面加执行的命令,可以将命令放到作业控制里执行。并且在bash中可以通过一些方法查看到协进程的pid和使用它的输入和输出。例子:

zorro@zorrozou-pc0 bash]$ cat coproc.sh
#!/bin/bash
#例一:简单命令使用
#简单命令使用不能通过NAME指定协进程的名字,此时进程的名字统一为:COPROC。
coproc tail -3 /etc/passwd
echo $COPROC_PID
exec 0<&${COPROC[0]}-
cat

#例二:复杂命令使用
#此时可以使用NAME参数指定协进程名称,并根据名称产生的相关变量获得协进程pid和描述符。

coproc _cat { tail -3 /etc/passwd; }
echo $_cat_PID
exec 0<&${_cat[0]}-
cat

#例三:更复杂的命令以及输入输出使用
#协进程的标准输入描述符为:NAME[1],标准输出描述符为:NAME[0]。

coproc print_username {
while read string
do
[ "$string" = "END" ] && break
echo $string | awk -F: '{print $1}'
done
}

echo "aaa:bbb:ccc" 1>&${print_username[1]}
echo ok

read -u ${print_username[0]} username

echo $username

cat /etc/passwd >&${print_username[1]}
echo END >&${print_username[1]}

while read -u ${print_username[0]} username
do
echo $username
done

执行结果:

[zorro@zorrozou-pc0 bash]$ ./coproc.sh
31953
jerry:x:1001:1001::/home/jerry:/bin/bash
systemd-coredump:x:994:994:systemd Core Dumper:/:/sbin/nologin
netdata:x:134:134::/var/cache/netdata:/bin/nologin
31955
jerry:x:1001:1001::/home/jerry:/bin/bash
systemd-coredump:x:994:994:systemd Core Dumper:/:/sbin/nologin
netdata:x:134:134::/var/cache/netdata:/bin/nologin
ok
aaa
root
bin
daemon
mail
ftp
http
uuidd
dbus
nobody
systemd-journal-gateway
systemd-timesync
systemd-network
systemd-bus-proxy
systemd-resolve
systemd-journal-remote
systemd-journal-upload
polkitd
avahi
colord
rtkit
gdm
usbmux
git
gnome-initial-setup
zorro
nvidia-persistenced
ntp
jerry
systemd-coredump
netdata

最后

本文主要介绍了一些bash编程的常用技巧,主要包括的知识点为:

  1. /dev/和/proc目录的使用。
  2. 函数和递归。
  3. 并发编程和flock。
  4. 受限bash。
  5. subshell。
  6. 协进程。

至此,我们的bash编程系列就算结束了。当然,shell其实到现在才刚刚开始。毕竟我们要真正实现有用的bash程序,还需要积累大量命令的使用。本文篇幅有限,就不探讨外部命令的详细使用方法和技巧了。希望这一系列内容对大家进一步深入了解bash编程有帮助。

如果有相关问题,可以在我的微博、微信或者博客上联系我。

请刷下面二维码捐赠0.99元。穷佐罗将持续为您奉上高质量的技术文章。多谢支持!

mm_facetoface_collect_qrcode_1465221734716


大家好,我是Zorro!

如果你喜欢本文,欢迎在微博上搜索“orroz”关注我,地址是:http://weibo.com/orroz

大家也可以在微信上搜索:Linux系统技术 关注我的公众号。

我的所有文章都会沉淀在我的个人博客上,地址是:http://liwei.life。

欢迎使用以上各种方式一起探讨学习,共同进步。

公众号二维码:

Zorro] icon


 

SHELL编程之内建命令

 

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和.实际上是同一个内建命令,它们的功能完全一样,只是两种不同写法。我们都应该见过这样一种写法,如:

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环境。如:加载环境变量配置脚本或从另一个脚本中引用其定义的函数时。我们可以通过如下例子来理解一下这个内建命令的作用:

[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从标准输入读取输字符串到一个变量中。用法如下:

[zorro@zorrozou-pc0 bash]$ cat input.sh 
#!/bin/bash

read -p "Login: " username

read -p "Passwd: " password

echo $username

echo $password

程序执行结果:

[zorro@zorrozou-pc0 bash]$ ./input.sh 
Login: zorro
Passwd: zorro
zorro
zorro

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

[zorro@zorrozou-pc0 bash]$ cat read.sh 
#!/bin/bash


read -a test

echo ${test[*]}

执行结果:

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

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

mapfile

readarray

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

[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

程序输出:

[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命令从一个文件描述符读取,如果不指定文件描述符,命令将默认从标准输入读取。所以很多人可能习惯用管道的方式读取,如:

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

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

echo 111 222 333 444 555| read -a test

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

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

echo

printf

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

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

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


如果本文有用,请刷下面二维码捐赠0.99元。穷佐罗将持续为您奉上高质量的技术文章。多谢支持!

mm_facetoface_collect_qrcode_1465221734716


作业控制

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

[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命令查看作业系统中有哪些进程在执行:

[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的方式控制,如:

[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。如:

[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命令:

[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作业控制进程脱离作业控制,变成一个“野”进程:

[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命令查看信号列表:

[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命令。方法如下:

[zorro@zorrozou-pc0 bash]$ cat trap.sh 
#!/bin/bash

trap 'echo hello' 2 15
trap 'exit 17' 3

while :
do
    sleep 1
done

trap命令的格式如下:

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

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

[zorro@zorrozou-pc0 bash]$ ./trap.sh 
^Chello
^Chello
^Chello
^Chello
^Chello
hello
hello

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

[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)状态的进程恢复运行。使用方法:

[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!"

执行效果:

[zorro@zorrozou-pc0 bash]$ ./suspend.sh 
echo 31833
Begin!
hmB
Enter suspend stat:

[1]+  Stopped                 ./suspend.sh

十秒之后:

[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会报错:

[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的运行时间又不一定多久,此时,我们就可以用这样一个办法:

[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,就变成了:

[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族的函数功能是一样的:将当前进程的执行镜像替换成指定进程的执行镜像。还是举例来看:

[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这样的方法来帮助我们处理类似的问题,如:

[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

以下为程序输出:

[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提供的一种纪录曾经去过的相关目录的缓存数据结构,可以方便的使操作者在多个深层次的目录中方便的跳转。使用演示:

显示当前目录栈:

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

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

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

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

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

将bbb目录加入目录栈:

[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目录:

[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目录:

[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目录:

[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目录:

[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开始。
从目录栈中推出一个目录,默认推出当前所在的目录:

[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一样:

[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是一个可能会被经常用到的内建命令。它的作用其实很简单,就是将指定的命令解析两次。可以这样理解这个命令:

首先我们定义一个变量:

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

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

[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:

[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相关环境。

希望这些内容对大家进一步深入了解bash编程有帮助。如果有相关问题,可以在我的微博、微信或者博客上联系我。

如果本文有用,请刷下面二维码捐赠0.99元。穷佐罗将持续为您奉上高质量的技术文章。多谢支持!

mm_facetoface_collect_qrcode_1465221734716


大家好,我是Zorro!

如果你喜欢本文,欢迎在微博上搜索“orroz”关注我,地址是:http://weibo.com/orroz

大家也可以在微信上搜索:Linux系统技术 关注我的公众号。

我的所有文章都会沉淀在我的个人博客上,地址是:http://liwei.life。

欢迎使用以上各种方式一起探讨学习,共同进步。

公众号二维码:

Zorro] icon


 

SHELL编程之特殊符号

SHELL编程之特殊符号

版权声明:

本文章内容在非商业使用前提下可无需授权任意转载、发布。

转载、发布请务必注明作者和其微博、微信公众号地址,以便读者询问问题和甄误反馈,共同进步。

微博ID:**orroz**

微信公众号:**Linux系统技术**

前言

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

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

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

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

重定向(REDIRECTION)

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

输入重定向

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

[zorro@zorrozou-pc0 bash]$ cat
hello 
hello
I am zorro!
I am zorro!

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

[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中输入:

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

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

[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语言数组)。在内核中,这个数组是用来管理一个进程打开的文件的对应关系的数组。就是说,对于任何一个进程来说,都有这样一个数组来管理它打开的文件,数组中的每一个元素和文件是映射关系,即:一个数组元素只能映射一个文件,而一个文件可以被多个数组元素所映射。

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

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

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

[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

这相当于只看报错信息。

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

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

[zorro@zorrozou-pc0 prime]$ find /etc -name passwd &> /dev/null

所有输出都不看,也可以写成”>&”。

[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

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

[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

语法:

<<[-]word
    here-document
delimiter

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

[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命令新建一个分区:

[root@zorrozou-pc0 prime]# cat fdisk.sh 
#!/bin/bash

fdisk /dev/sdb << EOF
n
p


w
EOF

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

Here strings

语法:

<<<word

使用方式:

[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都应该写一个文件路径,用来表示这个文件描述符的关联文件是谁。

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

#!/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命令。这种用法也叫做描述符魔术。某些特殊符号还有一些特殊用法,比如:

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

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

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

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

脚本参数处理

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

[zorro@zorrozou-pc0 bash]$ cat arg1.sh 
#!/bin/bash

echo $0
echo $1
echo $2
echo $3
echo $4
echo $#
echo $*
echo $?

执行结果:

[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}。这种写法的好处是对变量名字可以有更明确的界定,比如:

[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可以用来对参数进行位置处理,它会将所有参数都左移一个位置,可以用来进行参数处理。使用例子如下:

[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

执行效果:

[zorro@zorrozou-pc0 bash]$ ./shift.sh 111 222 333 444 555 666
111
222
333
444
555
666

其他的特殊变量还有:

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

$$:当前shell的PID。

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

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

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

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

数组操作

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

[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[@]}

命令行扩展

大括号扩展

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

[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

可能还有这样用的:

[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已经被赋值,则原值不变:

[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未被赋值,则赋值成=后面的值,其他行为不变:

[zorro@zorrozou-pc0 bash]$ echo $aaa

[zorro@zorrozou-pc0 bash]$ echo ${aaa:=1000}
1000
[zorro@zorrozou-pc0 bash]$ echo $aaa
1000

${aaa:?unset}

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

[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值:

[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个字符之后的字符串,变量原值不变。

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

${aaa:10:15}

第二个数字表示取多长:

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

${!B*}

${!B@}

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

[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}

取变量长度:

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

${parameter#word}

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

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

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

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

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

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

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

${parameter##word}

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

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

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

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

[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可以使用通配符,如:

[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}

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

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

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

大小写转换,如:

[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

有了以上符号后,很多变量内容的处理就不必再使用sed这样的重型外部命令处理了,可以一定程度的提高bash脚本的执行效率。

命令置换

命令置换这个概念就是在命令行中引用一个命令的输出给bash执行,就是我们已经用过的符号,如:
<pre><code>[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
</code></pre>
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

可以支持的运算符包括:

   id++ id--

   ++id --id
   - +    
   ! ~    
   **     
   * / %  
   + -    
   << >>  
   <= >= < >

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

另外可以进行算数运算的还有内建命令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

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

进程置换

<(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

这个符号会将相关命令的输出放到/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。

使用:

[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

关闭功能之后不能使用:

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

其他常用符号

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

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

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

[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

[[]]:这个符号基本跟内建命令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

可以支持的判断参数可以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

这个命令会被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

创建了一个叫做*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

如何删除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特殊字符的区别。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

希望这些内容对大家进一步深入了解bash编程有帮助。如果有相关问题,可以在我的微博、微信或者博客上联系我。


大家好,我是Zorro!

如果你喜欢本文,欢迎在微博上搜索“orroz”关注我,地址是:http://weibo.com/orroz

大家也可以在微信上搜索:Linux系统技术 关注我的公众号。

我的所有文章都会沉淀在我的个人博客上,地址是:http://liwei.life。

欢迎使用以上各种方式一起探讨学习,共同进步。

公众号二维码:

Zorro] icon