Bash Subshell变量作用域问题

今天研究apache ab这个测试工具,在网上看到压力测试shell脚本一文介绍了一个封装的bash脚本,用于多次测试返回requests per second的平均值,对脚本进行了简单的改写,将所有的测试输出进行记录。改写脚本在文章的最后。

改写过程中发现这样一个问题,比如写下面的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

result=""
cat abtest_temp.log | while read LINE
do
    result=`echo $LINE | grep 'Requests per second:'`
    if [ "$result" != "" ]
    then
        break
    fi
done
echo "result is "${result}

在读取abtest_temp.log文件内容后,result的值仍为空,这是因为bash遇到管道后会创建一个新的进程,于是result是subshell中的局域变量,subshell对变量的修改不会影响原shell中的变量。

subshell可以export父shell中的变量,但export出来的变量只是父shell中变量的一个拷贝,进行修改并不能影响到父shell。但反过来,父shell再次更改此变量时,subshell 再去读时,读到的是新值,而不是原来的值。参考bash man page中的说明:Each command in a pipeline is executed in its own subshell以及Each command in a pipeline is executed as a separate process (i.e., in a subshell).

对于这种情形有一些解决方法,这里给出两种:第一种是将subshell外需要访问的变量输出到临时文件中。第二种是使用命名管道。本质都是进程间通信的实现。

使用临时文件

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

result=""
cat abtest_temp.log | while read LINE
do
    result=`echo $LINE | grep 'Requests per second:'`
    if [ "$result" != "" ]
    then
        echo $result > .result_temp
        break
    fi
done
echo "result is "`cat .result_temp`

使用命名管道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash

result=""
mkfifo pipetem
(cat abtest_temp.log | while read LINE
do
    result=`echo $LINE | grep 'Requests per second:'`
    if [ "$result" != "" ]
    then
        echo $result > pipetem &
        break
    fi
done)
read result < pipetem
rm pipetem
echo "result is "${result}

对apache ab封装的测试脚本

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
#!/bin/bash

total_request=1000
concurrency=100
times=1

cmd_idx=1
param_count=$#
while [ $cmd_idx -lt $param_count ]
do
    cmd=$1
    shift 1 #remove $1
    case $cmd in
        -n)
            total_request=$1
            shift 1;;
        -c)
            concurrency=$1
            shift 1;;
        -t)
            times=$1
            shift 1;;
        *)
            echo "$cmd, support parameter: -n, -c, -t";;
    esac
    cmd_idx=`expr $cmd_idx + 2`
done

url=$1
if [ $url = '' ]; then
    echo 'the test url must be provided...'
    exit 2
fi

echo "Total Request: $total_request, Concurrency: $concurrency, URL: $url, Times: $times"

ab_dir=/usr/bin
ab_cmd="$ab_dir/ab -n $total_request -c $concurrency $url"

echo $ab_cmd
idx=1
rps_sum=0
max=-1
min=99999999
while [ $idx -le $times ]
do
    echo "start loop $idx"
    $ab_cmd >abtest_temp.log 2>&1
    cat abtest_temp.log | while read LINE
    do
        result=`echo $LINE | grep 'Requests per second:'`
        if [ "$result" != "" ]
        then
            echo $result > .result_temp
            break
        fi
    done
    result=`cat .result_temp`
    rm .result_temp
    result=`echo $result | awk -F ' ' '{ print $4 }' | awk -F '.' '{ print $1 }'`
    rps_sum=`expr $result + $rps_sum`
    if [ $result -gt $max ]; then
        max=$result
    fi
    if [ $result -lt $min ]; then
        min=$result
    fi
    idx=`expr $idx + 1`
done
echo "avg rps: "`expr $rps_sum / $times`
echo "min rps: $min"
echo "max rps: $max"

参考文章

压力测试shell脚本实例解析shell子进程(subshell )小心bash的管道

Comments