想要实现控制进程并发数,需要先了解下什么是文件描述符和命名管道
文件描述符是一个非负整数,内核需要通过文件描述符来访问文件。当我们在系统中打开已有的文件或新建文件时,内核每次都会给特定的进程返回一个文件描述符,通过这个文件描述符来对文件进行读写操作。系统中内核默认会为每个进程初始创建三个标准的文件描述符,分别是0(标准输入)
、1(标准输出)
、2(标准错误)
。可通过ll /proc/
查看指定进程所拥有的所有文件描述符
创建:exec 文件描述符 <> 文件名
使用:&文件描述符
删除:exec 文件描述符<&-
或exec 文件描述符>&-
# 创建仅可输入的文件描述符
exec 1000>test.txt # 创建仅可输入的文件描述符
echo hello >&1000 # 通过文件描述符写入数据
echo world >&1000
cat test.txt # 查看是否写入成功
hello
world
cat <&1000 # 从仅输入文件描述符读取数据失败
cat: -: Bad file descriptor
exec 1000<&- # 关闭文件描述符
# 创建仅可输出的文件描述符
touch test2.txt # 创建文件(输入文件描述符会自动创建文件,输出文件描述符不可以)
exec 1001<test2.txt # 创建仅可输出的文件描述符
echo hello >&1001 # 通过文件描述符写入数据报错
-bash: echo: write error: Bad file descriptor
exec 1000<&- # 关闭文件描述符
# 创建可读写的文件描述符
exec 1003<>test.txt # 创建可读写的文件描述符
cat <&1003 # 通过文件描述符读取数据
hello
world
echo hi >&1003 # 通过文件描述符写入数据
cat test.txt # 查看内容
hello
world
hi
exec 1000<&- # 关闭文件描述符
# 数据丢失案例
echo "init" > new.txt
exec 1000>new.txt # 创建仅输入文件描述符
echo "end" >&1000 # 通过文件描述符写入数据
exec 1000<&- # 关闭文件描述符
cat new.txt # 验证
end
# 通过追加解决数据丢失问题
exec 1000>>new.txt # 通过文件描述符追加数据
echo "start" >&1000
exec 1000<&-
cat new.txt
end
start
通过read -u
命令可以通过文件描述符获取文件内容,不过与cat
命令查看全部文件内容不同,read
命令默认一次仅读取一行数据,可以通过-n
选项指定读取行数。
示例:
cat test.txt # 准备一个有数据的文件
line1
line2
line3
exec 2000<text.txt # 创建仅输出述符
read -u2000 text # 读取一行并赋值给text变量
echo $text # 查看变量
line1
read -u2000 text # 读取一行并赋值给text变量
echo $text # 查看变量
line2
read -u2000 text # 读取一行并赋值给text变量
echo $text # 查看变量
line3
read -u2000 text # 当内容读取完后如果继续读取
echo $text # 查看变量
# 结果为空
cat <&2000 # 从文件描述符里读取所有数据
# 结果同样为空
通过以上演示可以得出,文件描述符并不是简单的对应一个文件,文件描述符中还包含有很多文件相关的信息,如权限、文件偏移量等。其中文件偏移量就像一个指针,默认指向第一行,当使用read
读取一行后,指针指向下一行,以此类推,直到文件读取完毕。
其实不仅read会导致指针偏移,cat <&100
同样也是,不同的是read
默认一次偏移一行,cat <&100
直接会导致指针指到文件末尾。除了以上方式,通过文件描述符输入数据同样会导致指针偏移。
提示:如果执行read -u2000
后面不跟变量名同样会导致指针偏移
管道是进程间通信的一种方式,使用|
会创建一个匿名管道,但是匿名管道只能实现父进程与子进程之间的数据交换,想要实现无关的进程间通信就需要命名管道,也叫FIFO
文件(First In First Out
先进先出)。命名管道有如下几个特性
FIFO
文件可以通过mknod
或mkfifo
命令创建示例:
mkfifo pipefile # 创建命名管道,不指定权限
mkfifo -m 664 pipefile2 # 创建命名管道,并指定权限
ls -l pipefile pipefile2 # 查看文件权限
prw-r--r-- 1 root root 0 Nov 17 11:01 pipefile
prw-rw-r-- 1 root root 0 Nov 17 11:21 pipefile2
echo "hello world" > pipefile # 写阻塞
cat pipefile # 读数据,并解除写阻塞
hello world
cat pipefile # 读阻塞
echo "hello fifo" > pipefile # 写数据,并解除读阻塞
通过文件描述符和命名管道就能实现控制进程的并发数,实现方式如下脚本所示。
#!/bin/bash
# 创建命名管道和文件描述符
mkfifo ch3
exec 12<> ch3
# 删除ch3文件防止下次执行影响执行(删除后不影响文件描述符使用)
rm -f ch3
# 通过文件描述符往命名管道中写入5行任意数据,用于控制进程数量
for i in {1..5};do
echo "" >&12
done
# 实现循环执行20次sleep命令,执行过程中保持同时有5个进程并发执行
# 首次并发执行5个进程后,这时的命名管道ch3中数据量是0条,会造成阻塞
# read -u12 表示每次从命名管道读取一行
# {...} & 表示括号内的一组命令后台执行
# echo "" >&12 表示当一个sleep进程执行完成后会往命名管道里添加一行新的数据
# 这时命名管道ch3中数据量是1条,会取消阻塞启动一个新的sleep进程
# 启动之后,ch3中数据量是0条,继续造成阻塞,当有新的sleep命令执行完成后,就会有新的sleep进程执行
# 循环往复,直到执行20次sleep命令
for j in {1..20};do
read -u12
{
echo "start sleep $j"
sleep 5
echo "stop sleep $j"
echo "" >&12
} &
done
# 等待所有后台进程执行完成
wait
执行结果如下
start sleep 1
start sleep 5
start sleep 2
start sleep 3
start sleep 4
stop sleep 5
stop sleep 1
start sleep 6
start sleep 7
stop sleep 2
stop sleep 4
stop sleep 3
start sleep 8
start sleep 9
start sleep 10
stop sleep 6
start sleep 11
stop sleep 7
start sleep 12
stop sleep 8
stop sleep 9
stop sleep 10
start sleep 13
start sleep 15
start sleep 14
stop sleep 11
start sleep 16
stop sleep 12
start sleep 17
stop sleep 13
stop sleep 15
start sleep 18
start sleep 19
stop sleep 14
start sleep 20
stop sleep 16
stop sleep 17
stop sleep 19
stop sleep 18
stop sleep 20
此文参考了丁明一大佬所著《Linux Shell核心编程指南》