Linux 文件描述符(file descriptor, fd)以及文件描述符操作dup(), dup2()

1.概述

在Linux系统中,一切皆可以看做是“文件”,这里“文件”包括普通文件、目录文件、链接文件和设备文件等。而文件描述符(file descriptor, 简称fd)是Linux内核所创建的索引,其目的为了高效管理已被打开的“文件”。其实,文件描述符就是一个非负整数(通常是小整数),用于指代被打开的“文件”,而所有对该“文件”的I/O操作都是通过该文件描述符来执行的。

通常,程序刚刚启动时,系统就已经占用了3个文件描述符:0(标准输入)、1(标准输出)和2(标准错误)。如果程序此时打开一个新文件或建立一个socket连接,其对应的文件描述符将会是3。

至于为什么会是3?原因:POSIX标准要求每次打开文件(socket)时,其分配的文件描述符必须是当前进程中最小可用的文件描述符。因此,在网络编程时,网络通信过程中稍不注意就可能造成“串话”现象。

POSIX标准输入输出的文件描述符,如下:

file descriptor purposePOSIXSTDIO
0标准输入STDIN_FILENOstdin
1标准输出STDOUT_FILENOstdout
2标准错误STDERR_FILENOstderr

2.文件描述符、文件指针、打开文件

在Linux系统中,每个文件描述符对应一个打开的文件,同时不同的文件描述符也可以指向同一个文件。相同的文件可以被不同的进程打开,每个进程中有对应的文件描述符(可以相同也可不相同);相同文件也可以在同一个进程中多次打开(对应不同的文件描述符)。上面已简单说明,为了高效管理打开的文件,Linux系统为每个进程维护了一个文件描述表,该表的值都是从0开始的,所以在不同的进城中可以看到相同的文件描述符。这种情况下,相同的文件描述符有可能指向同一个文件,也有可能指向不同的文件,具体情况要依据具体场景分析。要想理解具体的概况,需要了解和查看内核维护的3个数据结构:

  1. 进程级的文件描述符表
  2. 系统级的打开文件描述符表
  3. 文件系统的i-node表
进程级的文件描述符表中的每一项记录了该进程中单个文件描述符的相关信息。
  1. 控制文件描述符的一组标志(目前,此类标记只定义了一个,即close-on-exec)
  2. 对应打开文件句柄的应用(文件指针)
文件指针:C语言中使用文件指针作为I/O句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。其中,FILE结构包括一个缓冲区和一个文件描述符,而改文件描述符就是PCB中文件描述符表中的一个索引。因此,从某种意义上来说,文件指针就是文件句柄的句柄。


Linux内核对所有打开文件的维护有一个系统级的描述符表(open file descriptor table),或称之为打开文件表(open file table)。表中每条记录称为打开文件句柄(open file handler),一个打开文件句柄存储了与一个打开文件相关的全部信息,譬如:当前文件偏移量、文件访问模式、该文件i-node对象的引用、文件类型和访问权限等。


下图展示了文件描述符、打卡文件句柄(文件指针)、打开文件以及i-node之间的关系,其中,三个进程拥有多个打开的文件描述符。



其中:

Process 1中,文件描述符3和21都指向了同一个文件a,这可能是通过dup()、dup2()、fcntl()或对同一个文件多次调用open()函数形成的;

Process 1文件描述符21和Process 2中文件描述符35都指向文件a,但是却对应两个不同打开文件句柄(文件指针),二者打开模式不同(O_RDONLY与O_WRDONLY)。这种情形可能是调用fork()后创建子进程,或某个进程通过UNIX套接字将一个打开的文件描述符传递给另一个进程,或两个独立的进程分别open()打开同一个文件。

Process 1和Process 3中文件描述符3分别对应文件a和文件b。


3.文件描述符限制

通常,在涉及文件操作或网络通信编程时,初学者一般可能会遇到“Too many open files”问题。其原因是文件描述符是操作系统的一个重要资源,虽说系统内存有多少就可以打开多少文件描述符,但实际实现过程中内核一般都会做相应处理,一般打开文件数会是系统内存的10%(以KB来计算),称之为系统级限制。查看系统级的最大打开文件数可以使用sysctl -a | grep fs.file-max命令查看。与此同时,内核为了不让某一个进程消耗掉所有的文件资源,也会对单个进程最大打开文件数做默认处理,称之为用户级限制,默认值一般是1024。用户可以通过ulimit -a 和ulimit -n命令来查看和修改用户级限制。


用户也可以修改下limits.conf文件,永久更改系统文件描述符的最大值

vi /etc/security/limits.conf

在最后添加如下两行:

*       soft    nofile  65536
*       hard    nofile  65536


有关文件描述符的限制和设置,具体参见参考资料[1]和[2]。


4.dup()、dup2()文件描述符操作

dup()和dup2()都是对文件描述符的操作,程序可以通过系统调用来复制文件描述符。

函数原型:

#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
其中:

dup()用来复制oldfd所指的文件描述符。复制成功时,返回最小的尚未被使用的文件描述符;若出现错误则返回-1,且错误码存于errno中。而返回的新文件描述符和参数oldfd指向同一个文件,共享所有的索性、读写指针、各项权限或标志位等。


dup2()将参数newfd指向参数oldfd所指向的文件。若文件描述符newfd已经被程序使用(对已某个打开文件),系统则会将其关闭释放该文件描述符;若newfd和oldfd相等,dup2()将返回newfd,但不关闭newfd。dup2()调用成功返回新的文件描述符,出错则返回-1。



针对dup()和dup2(),下分别给出对应代码展示其相应的简单功能。


dup_test.c

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
  int fd_out = fileno(stdout);
  int fd_err = fileno(stderr);
  int fd_file = open("dup_test_file.log", O_RDWR | O_CREAT | O_APPEND | O_LARGEFILE, 0644);

  write(fd_out, "1: write message into fd_out.\n", strlen("1: write message into fd_out.\n"));
  write(fd_err, "1: write message into fd_err.\n", strlen("1: write message into fd_err.\n"));
  write(fd_file, "1: write message into fd_file.\n", strlen("1: write message into fd_file.\n"));

  printf("stdout's fd = %d.\n", fd_out);
  printf("stderr's fd = %d.\n", fd_err);
  printf("file's fd = %d.\n", fd_file);
  
  /**
   * int dup(int oldfd);
   * 用来复制oldfd所指的文件描述符.
   * 复制成功时,返回最小的尚未被使用的文件描述符;若出现错误则返回-1,且错误码存于errno.
   * 返回的新的文件描述符和参数oldfd指向同一个文件,共享所有的锁定、读写指针、各项权限或标志位等.
  **/

  int new_fd_err = dup(fd_err);
  if (-1 == new_fd_err)
  {
    printf("dup() failed.\n");
    return 1;
  }
  else
  {
    printf("new stderr's fd = %d.\n", new_fd_err);
    write(new_fd_err, "2: write message into new_fd_err(not close(fd_err)).\n", strlen("2: write message into new_fd_err(not close(fd_err)).\n"));
    close(fd_out);
    write(new_fd_err, "2: write message into new_fd_err(close(fd_err)).\n", strlen("2: write message into new_fd_err(close(fd_err)).\n"));
  }

  int new_fd_file = dup(fd_file);
  if (-1 == new_fd_file)
  {
    printf("dup() failed.\n");
    return 1;
  }
  else
  {
    printf("new file's fd = %d\n", new_fd_file);
    write(new_fd_file, "2: write message into new_fd_file(not close(fd_file)).\n", strlen("2: write message into new_fd_file(not close(fd_file)).\n"));
    close(fd_out);
    write(new_fd_file, "2: write message into new_fd_file(not close(fd_file)).\n", strlen("2: write message into new_fd_file(close(fd_file)).\n"));
  }

  return 0;
}

通过dup()对fd_err和fd_file进行复制,得到new_fd_err和new_fd_file,同时调用write()向新的文件描述符写入消息时,均会对应写到老的文件描述符指向的文件中,即使老的文件描述符已经关闭。


dup2_test.c

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
  int fd_out = fileno(stdout);
  int fd_err = fileno(stderr);

  int fd_file1 = open("dup2_test_file1.log", O_RDWR | O_CREAT | O_APPEND | O_LARGEFILE, 0644);

  write(fd_out, "1: write message into fd_out.\n", strlen("1: write message into fd_out.\n"));
  write(fd_err, "1: write message into fd_err.\n", strlen("1: write message into fd_err.\n"));
  write(fd_file1, "1: write message into file1.\n", strlen("1: write message into file1.\n"));
  
  printf("stdout's fd = %d.\n", fd_out);
  printf("stderr's fd = %d.\n", fd_err);
  printf("file1's fd = %d.\n", fd_file1);
  
  /**
   * int dup(int oldfd);
   * 用来复制oldfd所指的文件描述符.
   * 复制成功时,返回最小的尚未被使用的文件描述符;若出现错误则返回-1,且错误码存于errno.
   * 返回的新的文件描述符和参数oldfd指向同一个文件,共享所有的锁定、读写指针、各项权限或标志位等.
  **/
  
  /**
   * newfd与oldfd不相等,且newfd对应的文件只有一个文件描述符(引用计数为1)
   * 调用dup2()时会将newfd对应的文件关闭,同时newfd指向oldfd对应的文件(引用计数+1)
   * 此时,向newfd写入消息时,就会写到oldfd对应的文件
  **/
  int fd_ret = dup2(fd_file1, fd_err);
  printf("dup2() return value: fd_ret = %d.\n", fd_ret);
  // 下面这两种输出都会写到dup2_test_file1.log中
  write(fd_err, "2: write message into fd_err.\n", strlen("2: write message into fd_err.\n"));
  fprintf(stderr, "2: printf message into stderr.\n", strlen("2: printf message into stderr.\n"));

  /**
   * newfd与oldfd不相等,且newfd对应的文件不只一个文件描述符(引用计数大于1)
   * 调用dup2()时,newfd指向oldfd对应的文件(引用计数+1),而newfd对应的原文件并不关闭,仅将引用计数-1
   * 此时,向newfd写入消息时,就会写到oldfd对应的文件
  **/
  int fd_file2 = open("dup2_test_file2.log", O_RDWR | O_CREAT | O_APPEND | O_LARGEFILE, 0644);
  write(fd_file2, "3: write message into file2.\n", strlen("3: write message into file2.\n"));
  printf("file2's fd = %d.\n", fd_file2);

  int fd_file2_dup = dup(fd_file2);
  printf("file2's dup_fd = %d.\n", fd_file2_dup);

  fd_ret = dup2(fd_file1, fd_file2);
  printf("dup2() return value: fd_ret = %d.\n", fd_ret);

  // 写入file2_fd的消息将写入文件dup2_test_file1.log中
  write(fd_file2, "3: wriet message into file2_fd.\n", strlen("3: wriet message into file2_fd.\n"));
  // 写入file2_fd_dup的消息仍然写入dup2_test_file2.log中
  write(fd_file2_dup, "3: wriet message into file2_fd_dup.\n", strlen("3: wriet message into file2_fd_dup.\n"));

  /**
   * newfd与oldfd相等, dup2()不做任何事情,并返回newfd
  **/
  int fd = fd_file1;
  fd_ret = dup2(fd_file1, fd);
  printf("dup2() return value: fd_ret = %d.\n", fd_ret);
 
  //close(fd_file1);
  //把上面的操作在执行一遍,也会是同样的结果
  //同样也可以对stdout类似的操作,进而可以控制所有的标准输出都写到文件中

  return 1;
}


上述给出的例子里并未涉及网络通信(socket)所占用文件描述符时的操作使用,以及fork()子进程时,子进程和父进程间文件描述符的共享等。

5.总结

在Linux系统中,文件描述符是一个很重要的资源,尤其在设计文件操作或网络通信编程。而且,对于某些系统的调优,譬如mysql、java、squid等单进程处理大量并发请求的应用来说,如何设置和调优文件描述符限制,对系统的吞吐量和性能有至关重要的作用。

同时,自己设计并发程序时,也需要考虑用户级的文件描述符限制,进而可以控制自己的程序的并发性。


6.参考资料



  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值