开发者

Linux自定义文件描述符的操作指南

目录
  • 简单回顾文件描述符和重定向
  • 自定义文件描述符的基本操作
    • 进程产生的文件描述符
    • 使用命令创建的文件描述符
      • 创建文件描述符
      • 向文件描述符写入数据
      • 文件描述符读取数据
      • 关闭文件描述符
      • 创建只读、只写文件描述符
    • 文件描述符小例子
    • 总结

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

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

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

      # 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代码:

      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个小时。

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

      go build -o fdtests
      

      js后执行该可执行www.devze.com文件:

      # ./fdtests
      pid:  1362

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

      # 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----php--. 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系统中使用如下命令查询允许最大的文件描述符:

      ulimit -n
      

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

      ulimit -n 65535

      创建文件描述符

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

      exec 3<>/root/a.log
      

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

      # 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是一个虚拟目php录,不同的进程查看该目录会得到不一样的结果,即:该目录下的句柄,只能创建该句柄的进程所使用,其他进程均无法使用。

      向文件描述符写入数据

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

      echo "123" >& 3
      

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

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

      abcd 2>& 3
      javascript

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

      文件描述符读取数据

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

      cat <& 3
      

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

      # cat <& 3
      #
      

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

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

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

      关闭文件描述符

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

      exec 3>&-

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

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

      # 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,命令如下:

      # exec 5> /root/a1.log
      

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

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

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

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

      # cat <& 5
      cat: -: Bad file descriptor
      #

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

      # exec 6< /root/a1.log

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

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

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

      # echo '123' >& 5
      # cat <& 6
      123
      # cat <& 6
      #

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

      文件描述符小例子

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

      #!/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重新写入到管道中,以便实现循环读取。最后关闭文件描述符,删除管道文件。

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

      [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的文件描述符各项操作:

      创建只读的文件描述符:

      exec 3<filename

      创建只写的文件描述符:

      exec 3>filename
      

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

      exec 3<>filename

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

      <&3

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

      >&3
      

      最后是关闭文件描述符:

      3>&-
      

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

      以上就是Linux自定义文件描述符的操作指南的详细内容,更多关于Linux自定义文件描述符的资料请关注编程客栈(www.devze.com)其它相关文章!

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新运维

      运维排行榜