脚本编程中的锁机制与太空游戏实现
锁机制在脚本编程中的应用
在脚本编程中,当多个进程需要同时访问和修改同一资源时,可能会出现数据混乱的问题。锁机制就可以很好地解决这个问题,下面通过具体的脚本示例来详细介绍。
无锁脚本示例
以下是一个无锁脚本
domain-nolock.sh
,其功能是查询域名的创建日期、过期日期和 DNS 服务器信息,并将结果写入文件
/tmp/domains.txt
。
#!/bin/bash
KEYFILE=/tmp/domains.txt
MYDOMAIN=$1
echo “$MYDOMAIN Creation Date:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep -i created | cut -d”:” -f2- | tee -a $KEYFILE
sleep 2
echo “$MYDOMAIN Expiration Date:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep “Expiration Date:” | cut -d”:” -f2- | tee -a $KEYFILE
sleep 2
echo “$MYDOMAIN DNS Servers:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep “Name Server:” | cut -d”:” -f2- | \
grep -v “^$” | tee -a $KEYFILE
sleep 2
echo “... end of $MYDOMAIN information ...” | tee -a $KEYFILE
当我们在两个不同的交互式 shell 中分别运行这个脚本查询不同的域名时,会发现输出文件
/tmp/domains.txt
中的内容变得混乱不堪。这是因为两个脚本同时向同一个文件写入数据,导致信息交错。具体如下:
-
第一个 shell 运行
:
Instance One$ ./domain-nolock.sh example.com
example.com Creation Date:
1992-01-01
example.com Expiration Date:
13-aug-2011
example.com DNS Servers:
A.IANA-SERVERS.NET
B.IANA-SERVERS.NET
... end of example.com information ...
Instance One$
- 第二个 shell 运行 :
Instance Two$ ./domain-nolock.sh steve-parker.org
steve-parker.org Creation Date:
20-Jun-2000 13:48:46 UTC
steve-parker.org Expiration Date:
20-Jun-2011 13:48:46 UTC
steve-parker.org DNS Servers:
NS.123-REG.CO.UK
NS2.123-REG.CO.UK
... end of steve-parker.org information ...
Instance Two$
- 查看输出文件 :
Instance One$ cat /tmp/domains.txt
example.com Creation Date:
1992-01-01
steve-parker.org Creation Date:
example.com Expiration Date:
20-Jun-2000 13:48:46 UTC
13-aug-2011
steve-parker.org Expiration Date:
example.com DNS Servers:
20-Jun-2011 13:48:46 UTC
steve-parker.org DNS Servers:
A.IANA-SERVERS.NET
B.IANA-SERVERS.NET
NS.123-REG.CO.UK
NS2.123-REG.CO.UK
... end of example.com information ...
... end of steve-parker.org information ...
Instance One$
有锁脚本示例
为了解决上述问题,我们可以使用锁机制。以下是有锁脚本
domain.sh
:
#!/bin/bash
# LOCK is a global variable. For this usage, lock.myapp.$$ is not suitable.
# /var/run is suitable for root-owned processes; others may use /tmp or /var/tmp
# or their home directory or application filesystem.
# LOCK=/var/run/lock.myapp
LOCK=/tmp/lock.myapp
KEYFILE=/tmp/domains.txt
MYDOMAIN=$1
mydom=/tmp/${MYDOMAIN}.$$
# See kill(1) for the different signals and what they are intended to do.
trap cleanup 1 2 3 6
function release_lock
{
MYPID=$1
echo “Releasing lock.”
sed -i “/^${MYPID}$/d” $LOCK
}
function get_lock
{
DELAY=2
GOT_LOCK=0
MYPID=$1
while [ “$GOT_LOCK” -ne “1” ]
do
PID=
while [ -s “$LOCK” ]
do
PID=`cat $LOCK 2>/dev/null`
name=`ps -o comm= -p “$PID” 2>/dev/null`
if [ -z “$name” ]; then
echo “Process $PID has claimed the lock, but is not running.”
release_lock $PID
else
echo “Process $PID ($name) has already taken the lock:”
ps -fp $PID | sed -e 1d
date
echo
sleep $DELAY
let DELAY=”$DELAY + 1”
fi
done
# Store our PID in the lock file
echo $MYPID >> $LOCK
# If another instance also wrote to the lock, it will contain
# more than $$ and $PID
# PID could be blank, so surround it with quotes.
# Otherwise it is saying “-e $LOCK” and passing no filename,
grep -vw $MYPID $LOCK > /dev/null 2>&1
if [ “$?” -eq “0” ]; then
# If $? is 0, then grep successfully found something else in the file.
echo “An error occurred. Another process has taken the lock:”
ps -fp `grep -vw -e $MYPID -e “$PID” $LOCK`
# The other process can take care of itself.
# Relinquish access to the lock
# sed -i can do this atomically.
# Back off by sleeping a random amount of time.
sed -i “/^${$MYPID}$/d” $LOCK
sleep $((RANDOM % 5))
else
GOT_LOCK=1
# Claim exclusive access to the lock
echo $MYPID > $LOCK
fi
done
}
function cleanup
{
echo “$$: Caught signal: Exiting”
release_lock
exit 0
}
# Main Script goes here.
# You may want to do stuff without the lock here.
# Then get the lock for the exclusive work
get_lock $$
############
# Do stuff #
############
echo “$MYDOMAIN Creation Date:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep -i created | cut -d”:” -f2- | tee -a $KEYFILE
sleep 2
echo “$MYDOMAIN Expiration Date:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep “Expiration Date:” | cut -d”:” -f2- | tee -a $KEYFILE
sleep 2
echo “$MYDOMAIN DNS Servers:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep “Name Server:” | cut -d”:” -f2- | \
grep -v “^$” | tee -a $KEYFILE
sleep 2
echo “... end of $MYDOMAIN information ...” | tee -a $KEYFILE
echo >> $KEYFILE
# Then release the lock when you are done.
release_lock $$
# Again, there may be stuff that you will want to do after the lock is released
# Then cleanly exit.
exit 0
当我们使用这个有锁脚本时,情况就会有所不同。第一个实例运行时会获取锁,第二个实例在检测到锁被占用后,会等待锁被释放,然后再继续执行。具体如下:
-
第一个 shell 运行
:
Instance One$ ./domain.sh example.com
example.com Creation Date:
1992-01-01
example.com Expiration Date:
13-aug-2011
example.com DNS Servers:
A.IANA-SERVERS.NET
B.IANA-SERVERS.NET
... end of example.com information ...
Releasing lock.
Instance One$
- 第二个 shell 运行 :
Instance Two$ ./domain.sh steve-parker.org
Process 14228 (domain.sh) has already taken the lock:
steve 14228 12786 0 12:47 pts/7 00:00:00 /bin/bash ./domain.sh example.com
Fri Apr 22 12:47:11 BST 2011
Process 14228 (domain.sh) has already taken the lock:
steve 14228 12786 0 12:47 pts/7 00:00:00 /bin/bash ./domain.sh example.com
Fri Apr 22 12:47:14 BST 2011
Process 14228 (domain.sh) has already taken the lock:
steve 14228 12786 0 12:47 pts/7 00:00:00 /bin/bash ./domain.sh example.com
Fri Apr 22 12:47:17 BST 2011
Process 14228 (domain.sh) has already taken the lock:
steve 14228 12786 0 12:47 pts/7 00:00:00 /bin/bash ./domain.sh example.com
Fri Apr 22 12:47:21 BST 2011
steve-parker.org Creation Date:
20-Jun-2000 13:48:46 UTC
steve-parker.org Expiration Date:
20-Jun-2011 13:48:46 UTC
steve-parker.org DNS Servers:
NS.123-REG.CO.UK
NS2.123-REG.CO.UK
... end of example.com information ...
Releasing lock.
Instance Two$
- 查看输出文件 :
Instance One$ cat /tmp/domains.txt
example.com Creation Date:
1992-01-01
example.com Expiration Date:
13-aug-2011
example.com DNS Servers:
A.IANA-SERVERS.NET
B.IANA-SERVERS.NET
... end of example.com information ...
steve-parker.org Creation Date:
20-Jun-2000 13:48:46 UTC
steve-parker.org Expiration Date:
20-Jun-2011 13:48:46 UTC
steve-parker.org DNS Servers:
NS.123-REG.CO.UK
NS2.123-REG.CO.UK
... end of steve-parker.org information ...
Instance One$
从上述结果可以看出,使用锁机制后,输出文件的内容变得清晰有序,每个域名的信息都完整且独立。
锁机制总结
锁机制是确保代码实例能够独占资源的有效方法。一旦获取了锁,就可以进行诸如文件写入等操作,并且可以保证在该实例执行期间,其他进程不会干扰。在实际应用中,无论是否使用锁,第一个进程通常都不知道其他进程的存在,而使用锁的版本则能够确保对资源的独占访问。
基于 shell 脚本的太空游戏实现
在 shell 脚本中,除了实现系统管理功能,还可以开发一些有趣的游戏,比如下面要介绍的太空游戏。
游戏概述
这个太空游戏的灵感来源于经典的 70 年代街机游戏《太空侵略者》。游戏的目标是在外星人到达地球(屏幕底部)之前将其消灭。玩家可以使用 “a” 和 “l” 键左右移动飞船,按下 “f” 键发射炮弹,且每次只能发射一发炮弹。
使用的技术
该游戏使用了以下技术:
-
kill
、
trap
和
SIGALRM
进行计时。
- 高级使用
read
实现对按键的及时响应。
-
tput
控制终端。
- ANSI 颜色进行显示。
- 数组,特别是将数组传递给函数。
- 基本数学运算来计算位置和进行碰撞检测。
游戏概念
游戏背后的概念相对简单:
-
外星人移动
:外星人从左到右再返回移动,并且每次到达屏幕右侧时会向下移动一行。这通过不断增加
ceiling
变量来实现,外星人的位置由
(row*2) + ceiling
决定,乘以 2 是为了在每波外星人之间留出空白行。
-
激光炮
:激光炮由
cannonX
和
cannonY
两个变量表示,
cannonX
确保激光发射后保持垂直方向,且在发射后独立于飞船位置。在之前的炮弹击中外星人或到达屏幕顶部之前,不能发射新的炮弹。
-
数据结构
:飞船和激光炮的位置由简单的整数变量表示,每一行外星人由一个数组表示,该数组存储每个外星人被击中时的得分。当外星人被消灭时,数组中对应的值会变为 0,这表示该外星人已不存在,在绘制和碰撞检测时将不再考虑。
-
动画效果
:使用
aliens1
和
aliens2
两个数组以及取模 2 函数,使外星人看起来更具动画效果。
游戏实现中的问题及解决方法
-
实时交互
:实时读取按键而无需用户按回车键以及定期重绘屏幕是 shell 脚本实现此类游戏的难点。
read -n语法在较旧的 Unix 系统中不存在,但 GNU 系统和 Solaris 10 提供了该功能。外星人的实时更新通过SIGALRM信号实现,该信号每隔$DELAY秒唤醒脚本以刷新显示。 -
睡眠问题
:脚本中的
sleep $DELAY假设sleep可以接受非整数参数,但传统 Unix 的sleep最低只能睡眠 1 秒,这会使游戏在 Unix 系统上运行缓慢。可以通过每 X 次迭代睡眠一次来解决,随着$DELAY的逐渐减小,X 逐渐增大。 -
数组传递问题
:在 bash 中,数组不能直接作为参数传递给函数,也不能作为返回值返回。可以通过调用函数时使用
${a[@]},并处理$@来解决,同时要注意引号的使用。例如:
$ cat func-array.sh
#!/bin/bash
a=( one “two three” four five )
function myfunc
{
for value in “$@”
do
echo I was passed: $value
done
}
myfunc “${a[@]}”
$ ./func-array.sh
I was passed: one
I was passed: two three
I was passed: four
I was passed: five
$
潜在问题及优化
- 碰撞检测 :由于外星人的宽度大于一个单元格,碰撞检测是最难实现正确的部分。
-
屏幕刷新
:过多的屏幕刷新会导致游戏出现过度闪烁,影响游戏体验。调用
clear命令会花费较长时间,使显示变得非常闪烁。 -
性能优化
:在编写脚本时,将取模函数从调用外部
expr命令改为使用内置的(( … %2 ))构造,提高了改变外星人形状时的性能。将取模操作移出循环也能略微提高效率。
下面是游戏的结构流程图:
graph TD;
A[开始] --> B[隐藏光标];
B --> C[主循环];
C --> D{按键判断};
D -- "a或l" --> E[更新飞船位置并绘制];
D -- "f" --> F{炮弹是否可用};
F -- "是" --> G[设置炮弹位置];
C --> H[定时调用move函数];
H --> I[外星人移动];
I --> J{到达边缘};
J -- "是" --> K[改变方向并下移];
I --> L[调用drawrow函数];
L --> M{是否击中};
M -- "是" --> N[更新数组];
I --> O{外星人是否全灭};
O -- "是" --> P[显示祝贺消息并退出];
I --> Q[调用drawcannon函数];
C --> R{游戏结束};
R -- "是" --> S[显示光标并退出];
通过以上的介绍,我们了解了锁机制在脚本编程中的重要性以及如何在 shell 脚本中实现一个简单的太空游戏。这些技术不仅可以帮助我们解决实际的系统管理问题,还能让我们在脚本编程中发挥更多的创意。
脚本编程中的锁机制与太空游戏实现
太空游戏的详细结构与代码分析
游戏脚本的整体结构
该太空游戏脚本主要由四个核心函数和一个主循环构成,下面我们来详细分析其结构和功能。
-
主循环
:主循环位于脚本的底部,其主要功能是实时读取键盘输入。通过
read -n 1语句读取单个字符,根据不同的按键进行相应的操作:-
如果按下 “a” 或 “l” 键,分别表示向左或向右移动飞船,此时会调用
drawship函数更新飞船的位置并重新绘制。 -
如果按下 “f” 键,且炮弹未在使用中(
cannonY -eq 0),则设置炮弹的初始位置,cannonX与飞船当前的 X 位置相同,cannonY固定在屏幕底部。
-
如果按下 “a” 或 “l” 键,分别表示向左或向右移动飞船,此时会调用
以下是一个简单的示意代码,展示主循环的基本逻辑:
while true; do
read -n 1 key
case $key in
a)
# 向左移动飞船
update_ship_left
drawship
;;
l)
# 向右移动飞船
update_ship_right
drawship
;;
f)
if [ $cannonY -eq 0 ]; then
cannonX=$shipX
cannonY=$shipY
fi
;;
esac
done
-
drawship 函数
:该函数用于绘制飞船。当主循环检测到按键移动飞船时会调用此函数,同时在
move函数中也会被调用,以确保炮弹的更新能及时反映在屏幕上。-
首先,使用
printf语句清空屏幕底部的整行。 - 然后,根据炮弹是否处于发射状态,对飞船内的炮弹进行颜色编码,以提供实时的视觉反馈。
-
首先,使用
以下是
drawship
函数的简化示例:
function drawship {
# 清空底部行
printf "\e[${shipY};1H%s" "$(printf ' ' $(tput cols))"
# 根据炮弹状态绘制飞船
if [ $cannonY -eq 0 ]; then
# 炮弹未发射
printf "\e[${shipY};${shipX}H\e[32m<^>\e[0m"
else
# 炮弹已发射
printf "\e[${shipY};${shipX}H\e[31m<^>\e[0m"
fi
}
-
move 函数
:
move函数是游戏的核心逻辑之一,它使用SIGALRM信号实现定时调用自身。随着游戏的进行,DELAY变量会逐渐减小,使得外星人的移动速度越来越快。-
每次调用时,外星人会按照当前方向移动一格。当到达屏幕边缘时,改变移动方向,并在到达右侧边缘时向下移动一行(通过增加
ceiling变量实现)。 -
然后,针对每一行外星人调用
drawrow函数进行绘制,并处理碰撞检测。 - 接着,统计剩余的外星人数量,如果全部消灭,则显示祝贺消息并退出游戏。
-
最后,调用
drawcannon函数更新炮弹的位置。
-
每次调用时,外星人会按照当前方向移动一格。当到达屏幕边缘时,改变移动方向,并在到达右侧边缘时向下移动一行(通过增加
以下是
move
函数的简化逻辑:
function move {
# 设置下一次定时调用
trap "move" SIGALRM
alarm $DELAY
# 外星人移动
if [ $direction -eq 1 ]; then
# 向右移动
if [ $alienX -ge $(tput cols) - $alienWidth ]; then
direction=0
ceiling=$((ceiling + 1))
else
alienX=$((alienX + 1))
fi
else
# 向左移动
if [ $alienX -le 1 ]; then
direction=1
else
alienX=$((alienX - 1))
fi
fi
# 绘制每一行外星人
for ((i = 0; i < numRows; i++)); do
result=$(drawrow $i)
if [ $result -gt 0 ]; then
row$i[$result]=0
fi
done
# 统计剩余外星人数量
remaining=0
for ((i = 0; i < numRows; i++)); do
for value in "${row$i[@]}"; do
if [ $value -gt 0 ]; then
remaining=$((remaining + 1))
fi
done
done
if [ $remaining -eq 0 ]; then
echo "Congratulations! You have defeated all the aliens!"
exit 0
fi
# 更新炮弹位置
drawcannon
}
-
drawrow 函数
:
drawrow函数承担了大部分的绘制和碰撞检测工作。它接收一个参数,用于指定要绘制的外星人类型。- 在绘制外星人的过程中,检查是否有外星人与炮弹发生碰撞。如果发生碰撞,返回该外星人的索引;如果没有碰撞,返回 0。
- 根据返回的索引,更新存储外星人得分的数组,将对应位置的值设为 0,表示该外星人已被消灭。
以下是
drawrow
函数的简化示例:
function drawrow {
row=$1
index=0
for ((i = 0; i < ${#row$row[@]}; i++)); do
if [ ${row$row[$i]} -gt 0 ]; then
x=$((alienX + i * alienSpacing))
y=$((row * 2 + ceiling))
# 绘制外星人
printf "\e[${y};${x}H${aliens1[$i]}"
# 碰撞检测
if [ $cannonX -eq $x ] && [ $cannonY -eq $y ]; then
index=$((i + 1))
fi
fi
done
echo $index
}
- drawcannon 函数 :该函数用于更新炮弹的位置。首先,在炮弹的上一个位置绘制一个空格字符,然后计算新的位置(向上移动一格),并在新位置重新绘制炮弹。
以下是
drawcannon
函数的代码:
function drawcannon {
if [ $cannonY -gt 0 ]; then
# 清除上一个位置
printf "\e[${cannonY};${cannonX}H "
cannonY=$((cannonY - 1))
if [ $cannonY -gt 0 ]; then
# 绘制新位置
printf "\e[${cannonY};${cannonX}H|\e[0m"
else
cannonY=0
fi
fi
}
游戏脚本的优化与改进方向
虽然当前的太空游戏脚本已经实现了基本的功能,但仍有一些可以优化和改进的地方:
1.
性能优化
:可以进一步优化碰撞检测的逻辑,减少不必要的计算。例如,只对可能发生碰撞的区域进行检测,而不是对整个屏幕进行遍历。
2.
游戏难度调整
:除了简单地加快外星人的移动速度,还可以增加外星人的种类和攻击方式,提高游戏的难度和趣味性。
3.
用户界面改进
:可以添加更多的视觉效果,如爆炸动画、得分显示等,提升用户体验。
总结
通过本文的介绍,我们深入了解了脚本编程中的锁机制和基于 shell 脚本的太空游戏实现。锁机制在多进程访问共享资源时起着至关重要的作用,能够确保数据的一致性和完整性。而在 shell 脚本中实现太空游戏,展示了脚本语言的强大功能和灵活性,不仅可以用于系统管理,还可以开发有趣的交互式应用。
在实际应用中,我们可以根据具体需求对锁机制和游戏脚本进行进一步的优化和扩展。同时,这些技术也为我们提供了一个思路,即在不同的场景中充分发挥脚本编程的优势,解决各种实际问题。
以下是游戏主要函数调用关系的表格总结:
| 函数名称 | 调用位置 | 功能描述 |
| ---- | ---- | ---- |
|
drawship
| 主循环、
move
函数 | 绘制飞船,根据炮弹状态进行颜色编码 |
|
move
|
SIGALRM
信号触发 | 控制外星人移动,调用
drawrow
和
drawcannon
函数 |
|
drawrow
|
move
函数 | 绘制外星人并进行碰撞检测 |
|
drawcannon
|
move
函数 | 更新炮弹位置 |
希望本文能为你在脚本编程和游戏开发方面提供一些有用的参考和启发。
超级会员免费看
1170

被折叠的 条评论
为什么被折叠?



