linux 如何自定义文件描述符

linux 如何自定义文件描述符

简单回顾文件描述符和重定向

linux系统中,为每一个程序都定义了3个文件描述符,分别是标准输入:/dev/stdin、标准输出:/dev/stdout和错误输入:/dev/stderr,如果用文件描述符id表示,则从前到后分别是012

linux系统中,可以通过查询/proc/进程pid/fd下的012软连接文件,可以查看文件描述符的具体指向。

bash 复制代码
# ls -l /proc/$$/fd
total 0
lrwx------. 1 root root 64 Jun  4 22:54 0 -> /dev/pts/0
lrwx------. 1 root root 64 Jun  4 22:54 1 -> /dev/pts/0
lrwx------. 1 root root 64 Jun  4 22:54 2 -> /dev/pts/0
#

在程序初始运行中,系统会为分配这个3个文件描述符指向的设备或者文件,比如在上述例子中,都指向虚拟终端/dev/pts/0,而重定向则是修改文件描述符的指向,比如将标准输出指向到某个文件,而后续程序输出则会写到这个文件中,当然也可以指向其他设备。

除此之外,还能额外创建其他的文件描述符。

自定义文件描述符的基本操作

在系统中,任何进程都可以调用系统open函数,或者建立新的网络连接来创建文件描述符,而这些文件描述符都属于该进程自己。而在linux中,同样也可以创建文件描述符,而该文件描述符依然属于该进程,可以看看这二者之间的差别。

进程产生的文件描述符

关于进程创建文件描述符,举一个最简单的例子,比如我们编写如下的一段go代码:

go 复制代码
package main

import (
	"fmt"
	"net"
	"os"
	"time"
)

func main() {

	fmt.Println("pid: ", os.Getpid())

	for i := 0; i < 10; i++ {
		_, err := os.Create(fmt.Sprintf("file_%d", i))
		if err != nil {
			fmt.Println("open error", err)
		}
	}

	net.Dial("tcp", "192.168.1.1:22")
	net.Dial("tcp", "192.168.1.2:80")

	time.Sleep(time.Hour * 1)

}

上述代码,首先打印了进程的pid,而后在当前目录下新建了file_0file_9这10个文件,创建了的文件都没有进行close操作,也就是说,文件句柄还在进程中,然后使用net库创建了2个tcp连接,这是还是没有close操作,最后使用sleep睡眠了1个小时。

测试一下进程创建文件描述符号,首先进行编译:

bash 复制代码
go build -o fdtests

而后执行该可执行文件:

bash 复制代码
# ./fdtests
pid:  1362

得到了pid之后,新开一个终端,查看该进程的fd信息。

bash 复制代码
# ls -l /proc/1362/fd/
total 0
lrwx------. 1 root root 64 Jun  4 23:45 0 -> /dev/pts/0
lrwx------. 1 root root 64 Jun  4 23:45 1 -> /dev/pts/0
lrwx------. 1 root root 64 Jun  4 23:45 10 -> /root/file_5
lrwx------. 1 root root 64 Jun  4 23:45 11 -> /root/file_6
lrwx------. 1 root root 64 Jun  4 23:45 12 -> /root/file_7
lrwx------. 1 root root 64 Jun  4 23:45 13 -> /root/file_8
lrwx------. 1 root root 64 Jun  4 23:45 14 -> /root/file_9
lrwx------. 1 root root 64 Jun  4 23:45 15 -> socket:[22365]
lrwx------. 1 root root 64 Jun  4 23:45 16 -> socket:[22366]
lrwx------. 1 root root 64 Jun  4 23:45 2 -> /dev/pts/0
lrwx------. 1 root root 64 Jun  4 23:45 3 -> /root/file_0
lrwx------. 1 root root 64 Jun  4 23:45 4 -> anon_inode:[eventpoll]
lrwx------. 1 root root 64 Jun  4 23:45 5 -> anon_inode:[eventfd]
lrwx------. 1 root root 64 Jun  4 23:45 6 -> /root/file_1
lrwx------. 1 root root 64 Jun  4 23:45 7 -> /root/file_2
lrwx------. 1 root root 64 Jun  4 23:45 8 -> /root/file_3
lrwx------. 1 root root 64 Jun  4 23:45 9 -> /root/file_4
#

通过上面的例子,可以看到众多的文件句柄,其中012分别是系统创建的默认的句柄,还可以看到指向文件file_*的句柄,以及2个socket句柄,是最后的2个tcp连接。其中有2个特殊的句柄anon_inode,这个暂不讨论。

上述这些句柄都是属于进程的,其他进程是无法直接使用的,也需要调用系统open函数来实现自己的句柄才行。

使用命令创建的文件描述符

linux中,会为每一个进程都分配012 这三个文件描述符,包括使用的终端,而自定义的文件描述符必须从3开始,而linux建议使用39这几个文件描述符,当然,这只是建议,你可以不遵守,只要创建的文件描述符id不超过系统允许最大的值即可,在linux系统中使用如下命令查询允许最大的文件描述符:

bash 复制代码
ulimit -n

当然也可以动态修改它,比如将其修改为65535,只需要在n的后面添加即可:

bash 复制代码
ulimit -n 65535
创建文件描述符

linux中,创建一个可读可写的文件描述符,使用<>关键字即可,例如:

bash 复制代码
exec 3<>/root/a.log

如上命令创建了一个可读可写的文件描述符3,指向/root/a.log,在linux中创建的文件描述符是在/dev/fd目录中,例如:

bash 复制代码
# ls -l /dev/fd/
total 0
lrwx------. 1 root root 64 Jun  5 23:47 0 -> /dev/pts/0
lrwx------. 1 root root 64 Jun  5 23:47 1 -> /dev/pts/0
lrwx------. 1 root root 64 Jun  5 23:47 2 -> /dev/pts/0
lrwx------. 1 root root 64 Jun  5 23:47 3 -> /root/a.log
lr-x------. 1 root root 64 Jun  5 23:47 4 -> /proc/1852/fd
#

注意,该目录/dev/fd是一个虚拟目录,不同的进程查看该目录会得到不一样的结果,即:该目录下的句柄,只能创建该句柄的进程所使用,其他进程均无法使用。

向文件描述符写入数据

使用重定向写入内容,比如上面文件描述符id3,即:

bash 复制代码
echo "123" >& 3

注意,>&是一个关键字,表示向文件描述符写入数据,而3则表示文件描述符id,注意:没有>>&这种写法,这种写法是错误的。

如果要将一个标准错误的数据写入到对应的文件描述符中,需要用到2>&关键字,即:

bash 复制代码
abcd 2>& 3

都知道没有abcd这个命令,所以解释权会向错误输出报错找不到命令,使用2>&将该报错写入到文件描述符id3中。

文件描述符读取数据

使用<&关键字可以读取文件描述符的内容,比如:

bash 复制代码
cat <& 3

可以读取文件描述符id3的内容,但是实际执行后,你会发现,什么内容都没有,如:

bash 复制代码
# cat <& 3
#

这是因为linux中有一个叫做文件指针的概念(offset),在向该文件描述符写入内容的时候,已经同步将指针移动了到了最后,所以在读取的时候,什么内容也没有。需要重置offset为开头,即重新设置一下文件描述符:

bash 复制代码
# exec 3<>/root/a.log
# cat <& 3
123
-bash: abcd: command not found
#

如下即可读取到文件描述符指向文件的内容了。

关闭文件描述符

使用如下命令可以关闭id3的文件描述符:

bash 复制代码
exec 3>&-

其中,3>&-不能有任何空格,否则会报错。

关闭后,/dev/fd/文件中就没有相关文件的文件描述符了。

bash 复制代码
# ls -l /dev/fd/
total 0
lrwx------. 1 root root 64 Jun  5 23:49 0 -> /dev/pts/0
lrwx------. 1 root root 64 Jun  5 23:49 1 -> /dev/pts/0
lrwx------. 1 root root 64 Jun  5 23:49 2 -> /dev/pts/0
lr-x------. 1 root root 64 Jun  5 23:49 3 -> /proc/1293/fd
#
创建只读、只写文件描述符

上述是创建了一个可读写的文件测试服,文件指针会随着写入变化而变化,linux还允许创建只读、只写的文件描述符,比如:

创建只写文件描述符id5,指向文件/root/a1.log,命令如下:

bash 复制代码
# exec 5> /root/a1.log

可以使用查看/dev/fd/5权限,如下:

bash 复制代码
# ls -l /dev/fd/5
l-wx------. 1 root root 64 Jun  5 23:49 /dev/fd/5 -> /root/a1.log
#

其中它的权限为-wx,只有写和执行权限,没有读权限。

所以,若读取的话,会报错:

bash 复制代码
# cat <& 5
cat: -: Bad file descriptor
#

创建只读文件描述符id6,同样指向文件/root/a1.log,命令如下:

bash 复制代码
# exec 6< /root/a1.log

同样的,该文件权限只有读和执行权限,没有写权限。

bash 复制代码
# ls -l /dev/fd/6
lr-x------. 1 root root 64 Jun  5 23:47 /dev/fd/6 -> /root/a1.log
#

这个时候使用文件描述符5来写数据,使用文件描述符6来去读数据。

bash 复制代码
# echo '123' >& 5
# cat <& 6
123
# cat <& 6
#

读取完成后,文件指针会移动到读取后的位置,所以重复读取是没有用的。

文件描述符小例子

使用命名管道来限制多进程同时执行,代码如下:

bash 复制代码
#!/bin/bash

mkfifo pipe

exec 3<>./pipe

for i in $(seq 2)
do
        echo "${i}" >&3
done

for i in $(seq 10)
do
{
        read -u 3 id
        echo id: ${id} time: $(date +"%F %T") id: ${i}
        sleep 3
        echo ${id} >&3
}&
done

exec 3>&-

wait

rm -f pipe

echo "done"

上述脚本利用了管道来限制多进程同时执行,首先使用mkdifo是用来创建管道,而后定义了一个文件描述符id3来指向该管道文件,第一个for循环表示允许同时最多几个进程运行,上述定义的是2个,而后定义了10个进程来同时运行,使用read -u来读取文件描述符指向文件的内容,并且写入id变量,当read读取不到管道数据数据的时候,会阻塞当前进程,由于提前写入了2个数据,所以,只允许2个进程同时运行,并且执行完毕后,将id重新写入到管道中,以便实现循环读取。最后关闭文件描述符,删除管道文件。

所以上述脚本执行结果如下:

bash 复制代码
[root@localhost bash]# bash fd_test.sh
id: 1 time: 2025-06-05 23:47:46 id: 7
id: 2 time: 2025-06-05 23:47:46 id: 3
id: 1 time: 2025-06-05 23:47:49 id: 2
id: 2 time: 2025-06-05 23:47:49 id: 4
id: 1 time: 2025-06-05 23:47:52 id: 6
id: 2 time: 2025-06-05 23:47:52 id: 5
id: 1 time: 2025-06-05 23:47:55 id: 1
id: 2 time: 2025-06-05 23:47:55 id: 8
id: 1 time: 2025-06-05 23:47:58 id: 10
id: 2 time: 2025-06-05 23:47:58 id: 9
done
[root@localhost bash]#

可以通过输出发现,同一时间只有2个任务在同时运行。

总结

linux中,每个进程打开文件、建立网络连接,其实都是在文件描述符中增加相应的id,若超过系统设置的值,则会报错Too many open files

linux中,可以手动指定文件描述符,相关操作如下,比如创建id3的文件描述符各项操作:

创建只读的文件描述符:

bash 复制代码
exec 3<filename

创建只写的文件描述符:

bash 复制代码
exec 3>filename

创建可读写的文件描述符:

bash 复制代码
exec 3<>filename

进行文件描述符读取操作:

bash 复制代码
<&3

进行文件描述符写入操作:

bash 复制代码
>&3

最后是关闭文件描述符:

bash 复制代码
3>&-

最后介绍了一个小例子,使用文件描述符配合管道来实现控制shell脚本的同时运行。

相关推荐
dingdingfish3 天前
GNU Parallel 学习 - 第1章:How to read this book
bash·shell·gnu·parallel
似霰6 天前
Linux Shell 脚本编程——核心基础语法
linux·shell
似霰6 天前
Linux Shell 脚本编程——脚本自动化基础
linux·自动化·shell
偷学技术的梁胖胖yo7 天前
Shell脚本中连接数据库查询数据报错 “No such file or directory“以及函数传参数组
linux·mysql·shell
纵有疾風起16 天前
【Linux 系统开发】基础开发工具详解:软件包管理器、编辑器。编译器开发实战
linux·服务器·开发语言·经验分享·bash·shell
gis分享者18 天前
Shell 脚本中如何使用 here document 实现多行文本输入? (中等)
shell·脚本·document·多行·文本输入·here
柏木乃一18 天前
基础IO(上)
linux·服务器·c语言·c++·shell
angushine19 天前
CPU脚本并远程部署
shell
赵民勇23 天前
Linux/Unix中install命令全面用法解析
linux·shell
gis分享者24 天前
Shell 脚本中如何使用 trap 命令捕捉和处理信号(中等)
shell·脚本·信号·处理·trap·捕捉