? ? Shell概述
Shell是一種具備特殊功能的程序,它提供了用戶與內核進行交互操作的一種接口。它接收用戶輸入的命令,并把它送入內核去執行。內核是Linux系統的心臟,從開機自檢就駐留在計算機的內存中,直到計算機關閉為止,而用戶的應用程序存儲在計算機的硬盤上,僅當需要時才被調入內存。Shell是一種應用程序,當用戶登錄Linux系統時,Shell就會被調入內存去執行。Shell獨立于內核,它是連接內核和應用程序的橋梁,并由輸入設備讀取命令,再將其轉為計算機可以理解的機械碼,Linux內核才能執行該命令。
優勢
Shell腳本語言的好處是簡單、易學、易用,適合處理文件和目錄之類的對象,以簡單的方式快速完成某些復雜的事情通常是創建腳本的重要原則,腳本語言的特性可以總結為以下幾個方面:
語法和結構通常比較簡單。
學習和使用通常比較簡單,
通常以容易修改程序的“解釋”作為運行方式,而不需要“編譯。
程序的開發產能優于運行效能。
Shell腳本語言是Linux/Unix系統上一種重要的腳本語言,在Linux/Unix領域應用極為廣泛,熟練掌握Shell腳本語言是一個優秀的Linux/Unix開發者和系統管理員必經之路。利用Shell腳本語言可以簡潔地實現復雜的操作,而且Shell腳本程序往往可以在不同版本的Linux/Unix系統上通用。
Shell編程
基本格式
Shell腳本的文件名后綴通常是.sh (當然你也可以使用其他后綴或者沒有后綴,.sh是為了規范)
程序編寫格式:
[java] view plain copy#!/bin/bash
# 注釋使用#號
代碼示例:
[java] view plain copy//使用vi編輯器編寫shell腳本(a.sh不存在則會新建)
vi a.sh
進入vi編輯模式后編寫執行代碼
[java] view plain copy//固定格式,記住就可以了
#!/bin/bash
//執行的代碼
echo Hello World
賦予權限并執行:
[java] view plain copy//賦予可執行權限
chmod +x a.sh
//執行(調用/bin/bash執行a.sh腳本)
。/a.sh
執行結果:
下面是幾種運行情況:
[java] view plain copya.sh
這樣的話需要保證腳本具有執行權限并且在環境變量PATH中有(。),這樣在執行的時候會先從當前目錄查找。
[java] view plain copy./a.sh
只要保證這個腳本具有執行權限即可
[java] view plain copy/usr/local/a.sh
只要保證這個腳本具有執行權限即可
[java] view plain copybash a.sh
直接可以執行,甚至這個腳本文件中的第一行都可以不引入/bin/bash,它是將hello.sh作為參數傳給bash命令來執行的。
[java] view plain copybash -x /path/to/aa.sh
bash的單步執行
[java] view plain copybash -n /path/to/aa.sh
bash語法檢查
變量
變量不需要聲明,初始化不需要指定類型
變量命名
1、只能使用數字,字母和下劃線,且不能以數字開頭
2、變量名區分大小寫
3、建議命令要通俗易懂
注意:變量賦值是通過等號(=)進行賦值,在變量、等號和值之間不能出現空格。
顯示變量值使用echo命令(類似于java中的system.out) ,加上$變量名,也可以使用${變量名}
例如:
[java] view plain copyecho $JAVA_HOME
echo ${JAVA_HOME}
變量的申明和使用:
變量分類:
Shell變量有這幾類:本地變量、環境變量、局部變量、位置變量、特殊變量。
本地變量:
只對當前shell進程有效的,對當前進程的子進程和其它shell進程無效。
定義:VAR_NAME=VALUE
變量引用:${VAR_NAME} 或者 $VAR_NAME
取消變量:unset VAR_NAME
相當于java中的私有變量(private),只能當前類使用,子類和其他類都無法使用。
比如在一個bash命令窗口下再使用bash,則變成了子進程,本地變量不會被這個子進程所訪問。
環境變量:
自定義的環境變量對當前shell進程及其子shell進程有效,對其它的shell進程無效
定義:export VAR_NAME=VALUE
對所有shell進程都有效需要配置到配置文件中
[java] view plain copyvi /etc/profile
source /etc/profile
相當于java中的protected修飾符,對當前類,子孫類,以及同一個包下面可以共用。
和windows中的環境變量比較類似
自定義的環境變量:
局部變量:
在函數中調用,函數執行結束,變量就會消失
對shell腳本中某代碼片段有效
定義:local VAR_NAME=VALUE
相當于java代碼中某一個方法中定義的局部變量,只對這個方法有效。
位置變量:
比如腳本中的參數:
$0:腳本自身
$1:腳本的第一個參數
$2:腳本的第二個參數
相當于java中main函數中的args參數,可以獲取外部參數。
特殊變量:
$?:接收上一條命令的返回狀態碼
返回狀態碼在0-255之間
$#:參數個數
$*:或者$@:所有的參數
$$:獲取當前shell的進程號(PID)(可以實現腳本自殺)(或者使用exit命令直接退出也可以使用exit [num])
引號
Shell編程中有三類引號:單引號、雙引號、反引號。
‘’單引號不解析變量
[java] view plain copyecho ‘$name’
“”雙引號會解析變量
[java] view plain copyecho “$name”
``反引號是執行并引用一個命令的執行結果,類似于$(。。。)
[java] view plain copyecho `$name`
示例:
循環
for循環
通過使用一個變量去遍歷給定列表中的每個元素,在每次變量賦值時執行一次循環體,直至賦值完成所有元素退出循環
格式1
[java] view plain copyfor ((i=0;i《10;i++))
do
。。。
Done
格式2
[java] view plain copyfor i in 0 1 2 3 4 5 6 7 8 9
do
。。。
Done
格式3
[java] view plain copyfor i in {0..9}
do
。。。
done
注意:for i in {0..9} 等于for i in {0..9..1} , 第三個參數為跨步。
例如:
{0..9..2} 表示 0,2,4,6,8
while循環
適用于循環次數未知,或不便用for直接生成較大的列表時
格式:
[java] view plain copywhile 測試條件
do
循環體
done
如果測試條件為“真”,則進入循環,測試條件為假,則退出循環。
打印結果為0~9.
循環控制
循環控制命令——break
break命令是在處理過程中跳出循環的一種簡單方法,可以使用break命令退出任何類型的循環,包括while循環和for循環
循環控制命令——continue
continue命令是一種提前停止循環內命令,而不完全終止循環的方法,這就需要在循環內設置shell不執行命令的條件
條件
bash條件測試
格式:
[java] view plain copytest EXPR
[ EXPR ]:注意中括號和表達式之間的空格
整型測試:
-gt:大于:
-lt:小于
-ge:大于等于
-le:小于等于
-eq:等于
-ne:不等于
例如[ $num1 -gt $num2 ]或者test $num1 -gt $num2
字符串測試:
=:等于,例如判斷變量是否為空 [ “$str” = “” ] 或者[ -z $str ]
!=:不等于
判斷
if判斷:
單分支
[java] view plain copy if 測試條件;then
選擇分支
fi
雙分支
[java] view plain copyif 測試條件
then
選擇分支1
else
選擇分支2
fi
多分支
[java] view plain copyif 條件1; then
分支1
elif 條件2; then
分支2
elif 條件3; then
分支3
。。。
else
分支n
i
雙分支示例:
Case判斷
有多個測試條件時,case語句會使得語法結構更清晰
格式:
[java] view plain copycase 變量引用 in
PATTERN1)
分支1
;;
PATTERN2)
分支2
;;
。。。
*)
分支n
;;
esac
PATTERN :類同于文件名通配機制,但支持使用|表示或者
a|b:a或者b
*:匹配任意長度的任意字符
?:匹配任意單個字符
[a-z]:指定范圍內的任意單個字符
示例:
算術運算
[java] view plain copylet varName=算術表達式
varName=$[算術表達式]
varName=$((算術表達式))
varName=`expr $num1 + $num2`
使用這種格式要注意兩個數字和+號中間要有空格。
示例:
邏輯運算符
if [ 條件A && 條件B ] 在shell中怎么寫?
if [ 條件A && 條件B ];then 是不對的
解決方法:
(1)需要用到shell中的邏輯操作符
-a 與
-o 或
! 非
如if [ 條件A -a 條件B ]
(2)if [ 條件A ] && [條件B ]
(3)if((A&&B))
(4)if [[ A&&B ]]
自定義函數
格式:
[java] view plain copyfunction 函數名(){
。。。
}
引用自定義函數文件時,使用source func.sh
有利于代碼的重用性
函數傳遞參數(可以使用類似于Java中的args,args[1]代表Shell中的$1)
函數的返回值,只能是數字
read
read命令接收標準輸入(鍵盤)的輸入,或者其他文件描述符的輸入。得到輸入后,read命令將數據放入一個標準變量中。
格式
[java] view plain copyread VAR_NAME
read如果后面不指定變量,那么read命令會將接收到的數據放置在環境變量REPLY中
[java] view plain copy#表示輸入時的提示字符串:
read -p “Enter your name:” VAR_NAME
[java] view plain copy# -t表示輸入等待的時間
read -t 5 -p “enter your name:” VAR_NAME
[java] view plain copy# -s 表示安全輸入,鍵入密碼時不會顯示
read -s -p “Enter your password: ” pass
declare
用來限定變量的屬性
-r 只讀
-i 整數:某些算術計算允許在被聲明為整數的變量中完成,而不需要特別使用expr或let來完成。
-a 數組
示例:
字符串操作
獲取長度:
[java] view plain copy${#VAR_NAME}
字符串截取
[java] view plain copy${variable:offset:length}或者${variable:offset}
取尾部的指定個數的字符
[java] view plain copy${variable: -length}:注意冒號后面有空格
大小寫轉換
小--》大:
[java] view plain copy${variable^^}
大--》小:
[java] view plain copy${variable,,}
示例:
數組
定義:declare -a:表示定義普通數組
特點
支持稀疏格式
僅支持一維數組
數組賦值方式
一次對一個元素賦值a[0]=$RANDOM
一次對多個元素賦值a=(a b c d)
按索引進行賦值a=([0]=a [3]=b [1]=c)
使用read命令read -a ARRAY_NAME查看元素
[java] view plain copy${ARRAY[index]}:查看數組指定角標的元素
${ARRAY}:查看數組的第一個元素
${ARRAY[*]}或者${ARRAY[@]}:查看數組的所有元素
獲取數組的長度
[java] view plain copy${#ARRAY[*]}
${#ARRAY[@]}
獲取數組內元素的長度
[java] view plain copy${#ARRAY[0]}
注意:${#ARRAY[0]}表示獲取數組中的第一個元素的長度,等于${#ARRAY}
從數組中獲取某一片段之內的元素(操作類似于字符串操作)
格式:
[java] view plain copy${ARRAY[@]:offset:length}
offset:偏移的元素個數
length:取出的元素的個數
${ARRAY[@]:offset:length}:取出偏移量后的指定個數的元素
${ARRAY[@]:offset}:取出數組中偏移量后的所有元素
數組刪除元素:
[java] view plain copyunset ARRAY[index]
示例:
其他命令
date
顯示當前時間
格式化輸出 +%Y-%m-%d
格式%s表示自1970-01-01 00:00:00以來的秒數
指定時間輸出 --date=‘2009-01-01 11:11:11’
指定時間輸出 --date=‘3 days ago’ (3天之前,3天之后可以用-3)
示例:
后臺運行腳本
在腳本后面加一個&
[java] view plain copytest.sh &
這樣的話雖然可以在后臺運行,但是當用戶注銷(logout)或者網絡斷開時,終端會收到Linux HUP信號(hangup)信號從而關閉其所有子進程
nohup命令
不掛斷的運行命令,忽略所有掛斷(hangup)信號
[java] view plain copynohup test.sh &
nohup會忽略進程的hangup掛斷信號,所以關閉當前會話窗口不會停止這個進程的執行。
nohup會在當前執行的目錄生成一個nohup.out日志文件
標準輸入、輸出、錯誤、重定向
標準輸入、輸出、錯誤可以使用文件描述符0、1、2引用
使用重定向可以把信息重定向到其他位置
ls 》file 或者 ls 1》file(ls 》》file)
lk 2》file(lk是一個錯誤命令)
ls 》file 2》&1
ls 》 /dev/null(把輸出信息重定向到無底洞)
例子:
[java] view plain copycommand 》/dev/null 2》&1
Crontab定時器
linux下的定時任務
編輯使用crontab -e
一共6列,分別是:分 時 日 月 周 命令
查看crontab執行日志
[java] view plain copytail -f /var/log/cron
必須打開rsyslog服務cron文件中才會有執行日志(service rsyslog status)
[java] view plain copytail -f /var/spool/mail/root(查看crontab最近的執行情況)
查看cron服務狀態
[java] view plain copyservice crond status
啟動cron服務
[java] view plain copyservice crond start
小結及示例:
基本格式 :
* * * * * command
分 時 日 月 周 命令
第1列表示分鐘1~59 每分鐘用*或者 */1表示
第2列表示小時1~23(0表示0點)
第3列表示日期1~31
第4列表示月份1~12
第5列標識號星期0~6(0表示星期天)
第6列要運行的命令
crontab文件的一些例子:
30 21 * * * /usr/local/etc/rc.d/lighttpd restart
上面的例子表示每晚的21:30重啟apache。
45 4 1,10,22 * * /usr/local/etc/rc.d/lighttpd restart
上面的例子表示每月1、10、22日的4 : 45重啟apache。
10 1 * * 6,0 /usr/local/etc/rc.d/lighttpd restart
上面的例子表示每周六、周日的1 : 10重啟apache。
0,30 18-23 * * * /usr/local/etc/rc.d/lighttpd restart
上面的例子表示在每天18 : 00至23 : 00之間每隔30分鐘重啟apache。
0 23 * * 6 /usr/local/etc/rc.d/lighttpd restart
上面的例子表示每星期六的11 : 00 pm重啟apache。
* */1 * * * /usr/local/etc/rc.d/lighttpd restart
每一小時重啟apache
* 23-7/1 * * * /usr/local/etc/rc.d/lighttpd restart
晚上11點到早上7點之間,每隔一小時重啟apache
0 11 4 * mon-wed /usr/local/etc/rc.d/lighttpd restart
每月的4號與每周一到周三的11點重啟apache
0 4 1 jan * /usr/local/etc/rc.d/lighttpd restart
一月一號的4點重啟apache
ps和jps
ps:用來顯示進程的相關信息
ps顯示當前shell啟動的所有進程
ps -e顯示系統中所有進程
ps -ef|grep java
jps:類似linux的ps命令,不同的是ps是用來顯示所有進程,而jps只顯示java進程,準確的說是顯示當前用戶已啟動的部分java進程信息,信息包括進程號和簡短的進程command。
問題:某個java進程已經啟動,用jps卻顯示不了該進程進程號,使用ps -ef|grep java卻可以看到?
java程序啟動后,默認(請注意是默認)會在/tmp/hsperfdata_userName目錄下以該進程的id為文件名新建文件,并在該文件中存儲jvm運行的相關信息,其中的userName為當前的用戶名,/tmp/hsperfdata_userName目錄會存放該用戶所有已經啟動的java進程信息。而jps、jconsole、jvisualvm等工具的數據來源就是這個文件(/tmp/hsperfdata_userName/pid)。所以當該文件不存在或是無法讀取時就會出現jps無法查看該進程號。
原因:1,磁盤讀寫、目錄權限問題。2,臨時文件丟失,被刪除或是定期清理。3,java進程信息文件存儲地址被設置,不在/tmp目錄下
登錄Shell和交互shell
交互式的:顧名思義,這種shell中的命令時由用戶從鍵盤交互式地輸入的,運行的結果也能夠輸出到終端顯示給用戶看。
非交互式的:這種shell可能由某些自動化過程啟動,不能直接從請求用戶的輸入,也不能直接輸出結果給終端用戶看。輸出最好寫到文件。比如使用Shell腳本。
登錄式:意思是這種是在某用戶由/bin/login登陸進系統后啟動的shell,跟這個用戶綁定。這個shell是用戶登陸后啟動的第一個進程。login進程在啟動shell時傳遞第0個參數指明shell的名字,該參數第一個字符為“-”,指明這是一個login shell。比如對bash而言,啟動參數為“-bash”。
非登錄式:不需login而由某些程序啟動的shell。傳遞給shell的參數,是沒有‘-’前綴的。還以Bash為例,當以非login方式啟動時,它會調用~/.bashrc,隨后~/.bashrc中調用/etc/bashrc,最后/etc/bashrc調用所有/etc/profile.d目錄下的腳本。
一旦打開一個交互式login shell,或者以--login選項登錄的非交互式shell,都會首先加載并執行/etc/profile中的命令,然后再依次加載~/.bash_profile, ~/.bash_login, 和~/.profile中的命令。
當bash以login shell啟動時,它會執行/etc/profile中的命令,然后/etc/profile調用/etc/profile.d目錄下的所有腳本;然后執行~/.bash_profile,~/.bash_profile調用~/.bashrc,最后~/.bashrc又調用/etc/bashrc。要識別一個shell是否為login shell,只需在該shell下執行echo $0。
注意: /etc/profile中的設置只對Login Shell生效,而crontab運行腳本的shell環境是non-login的,不會加載/etc/profile的設置。
Shell應用示例
根據時間創建文件夾
需求:創建10個目錄,目錄名稱以當天時間開頭,后面拼上目錄編碼
例如:1970-01-01_1
編寫腳本monitor.sh
持續觀察服務器每天的運行狀態,需要結合shell腳本程序和計劃任務,定期跟蹤記錄不同時段服務器的cpu負載,內存,交換空間,磁盤使用量等信息
[java] view plain copy#!/bin/bash
#this is the second script!
day_time=`date+“%F %R”`
cpu_test=`uptime`
mem_test=`free -m | grep “mem” | awk ‘{print $2}’`
swap_test=`free -m | grep “mem” | awk ‘{print $4}’`
disk_test=`df -hT`
user_test=`last -n 10`
echo “now is $day_time”
echo “%cpu is $cpu_test”
echo “Numbet of Mem size(MB) is $mem_test”
echo “Number of swap size(MB) is $swap_test”
echo “the disk shiyong qingkuang is $disk_test”
echo “the users login qingkuang is $user_test”
設置cron任務
[java] view plain copy*/15 * * * * bash /monitor.sh
55 23 * * * tar cxf /var/log/runrec /var/log/running.today && --remove-files
SHELL編程之常用技巧
/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/sda12
設備文件的主設備號對應了這種設備所使用的驅動是哪個,而輔助設備號則表示使用同一種驅動的設備編號。我們可以使用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 harddisk123
這樣我們就創建了一個設備文件叫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/s1234
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 《&51234567891011121314
ipaddr的部分還可以寫一個主機名。大家可以用此腳本分別在本機打開web服務和不打開的情況下分別執行觀察是什么效果。
/proc是另一個我們經常使用的目錄。這個目錄完全是內核虛擬的。內核將一些系統信息都放在/proc目錄下一文件和文本的方式顯示出來,如:/proc/cpuinfo、/proc/meminfo。我們可以使用man 5 proc來查詢這個目錄下文件的作用。
函數和遞歸
我們已經接觸過函數的概念了,在bash編程中,函數無非是將一串命令起了個名字,后續想要調用這一串命令就可以直接寫函數的名字了。在語法上定義一個函數的方法是:
name () compound-command [redirection]
function name [()] compound-command [redirection]12
我們可以加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!”12345678910111213141516171819202122232425262728293031323334
我們帶-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!1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
觀察整個執行過程可以發現,函數的參數適用方法跟腳本一樣,都可以使用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 $11234567891011121314151617
這個腳本可以遍歷一個目錄下所有子目錄中的非目錄文件。關于遞歸,還有一個經典的例子,fork炸彈:
。(){ 。|.& };.1
這一堆符號看上去很令人費解,我們來解釋一下每個符號的含義:根據函數的定義語法,我們知道。(){}的意思是,定義一個函數名子叫“。”。雖然系統中又個內建命令也叫。,就是source命令,但是我們也知道,當函數和內建命令名字沖突的時候,bash首先會將名字當成是函數來解釋。在{}包含的函數體中,使用了一個管道連接了兩個點,這里的第一個。就是函數的遞歸調用,我們也知道了使用管道的時候會打開一個subshell的子進程,所以在這里面就遞歸的打開了子進程。{}后面的分號只表示函數定義完畢的結束符,在之后就是調用函數名執行的。,之后函數開始遞歸的打開自己,去產生子進程,直到系統崩潰為止。
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 $countfile12345678910111213141516171819202122232425
我們再來看看這個程序的執行結果:
[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
5123456789101112
多次執行之后,每次得到的結果都不一樣,也沒有一次是正確的結果。這就是典型的競爭條件引起的問題。當多個進程并發的時候,如果使用的共享的資源,就有可能會造成這樣的問題。這里的競爭調教就是:當某一個進程讀出文件值為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
10012345678910111213141516171819202122232425262728293031323334
對臨界區代碼進行加鎖處理之后,程序執行結果正確了。仔細思考一下程序之后就會發現,這里所謂的臨界區代碼由加鎖前的并行,變成了加鎖后的串行。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 01234567891011121314151617181920
-n參數可以讓flock命令以非阻塞方式探測一個文件是否已經被加鎖,所以可以使用互斥鎖的特點保證腳本運行的唯一性。腳本退出的時候鎖會被釋放,所以這里可以不用顯式的使用flock解鎖。flock除了-u參數指定文件描述符鎖文件以外,還可以作為執行命令的前綴使用。這種方式非常適合直接在crond中方式所要執行的腳本重復執行。如:
*/1 * * * * /usr/bin/flock -xn /tmp/script.lock -c ‘/home/bash/script.sh’1
關于flock的其它參數,可以man flock找到說明。
受限bash
以受限模式執行bash程序,有時候是很有必要的。這種模式可以保護我們的很多系統環境不受bash程序的誤操作影響。啟動受限模式的bash的方法是使用-r參數,或者也可以rbash的進程名方式執行bash。受限模式的bash和正常bash時間的差別是:
不能使用cd命令改變當前工作目錄。
不能改變SHELL、PATH、ENV和BASH_ENV環境變量。
不能調用含有/的命令路徑。
不能使用。執行帶有/字符的命令路徑。
不能使用hash命令的-p參數指定一個帶斜杠\的參數。
不能在shell環境啟動的時候加載函數的定義。
不能檢查SHELLOPTS變量的內容。
不能使用》, 》|, 《》, 》&, &》和 》》重定向操作符。
不能使用exec命令使用一個新程序替換當前執行的bash進程。
enable內建命令不能使用-f、-d參數。
不可以使用enable命令打開或者關閉內建命令。
command命令不可以使用-p參數。
不能使用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: restricted12345678
subshell
我們前面接觸過subshell的概念,我們之前說的是,當一個命令放在()中的時候,bash會打開一個子進程去執行相關命令,這個子進程實際上是另一個bash環境,叫做subshell。當然包括放在()中執行的命令,bash會在以下情況下打開一個subshell執行命令:
使用&作為命令結束提交了作業控制任務時。
使用|連接的命令會在subshell中打開。
使用()封裝的命令。
使用coproc(bash 4.0版本之后支持)作為前綴執行的命令。
要執行的文件不存在或者文件存在但不具備可執行權限的時候,這個執行過程會打開一個subshell執行。
在subshell中,有些事情需要注意。subshell中的$$取到的仍然是父進程bash的pid,如果想要取到subshell的pid,可以使用BASHPID變量:
[zorro@zorrozou-pc0 bash]$ echo $$ ;echo $BASHPID && (echo $$;echo $BASHPID)
5484
5484
5484
2458412345
可以使用BASH_SUBSHELL變量的值來檢查當前環境是不是在subshell中,這個值在非subshell中是0;每進入一層subshell就加1。
[zorro@zorrozou-pc0 bash]$ echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL))
0
1
21234
在subshell中做的任何操作都不會影響父進程的bash執行環境。subshell除了PID和trap相關設置外,其他的環境都跟父進程是一樣的。subshell的trap設置跟父進程剛啟動的時候還沒做trap設置之前一樣。
協進程coprocess
在bash 4.0版本之后,為我們提供了一個coproc關鍵字可以支持協進程。協進程提供了一種可以上bash移步執行另一個進程的工作模式,實際上跟作業控制類似。嚴格來說,bash的協進程就是使用作業控制作為實現手段來做的。它跟作業控制的區別僅僅在于,協進程的標準輸入和標準輸出都在調用協進程的bash中可以取到文件描述符,而作業控制進程的標準輸入和輸出都是直接指向終端的。我們來看看使用協進程的語法:
coproc [NAME] command [redirections]1
使用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
done123456789101112131415161718192021222324252627282930313233343536373839404142
執行結果:
[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
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
netdata1234567891011121314151617181920212223242526272829303132333435363738394041
最后
本文主要介紹了一些bash編程的常用技巧,主要包括的知識點為:
/dev/和/proc目錄的使用。
函數和遞歸。
并發編程和flock。
受限bash。
subshell。
協進程。
至此,我們的bash編程系列就算結束了。當然,shell其實到現在才剛剛開始。畢竟我們要真正實現有用的bash程序,還需要積累大量命令的使用。
評論
查看更多