Linux 文件描述符(file descriptor, fd)以及文件描述符操作dup(), dup2()
文件描述符(file descriptor, 简称fd)是Linux内核所创建的索引,其目的为了高效管理已被打开的“文件”。其实,文件描述符就是一个非负整数(通常是小整数),用于指代被打开的“文件”,而所有对该“文件”的I/O操作都是通过该文件描述符来执行的。
1.概述
在Linux系统中,一切皆可以看做是“文件”,这里“文件”包括普通文件、目录文件、链接文件和设备文件等。而文件描述符(file descriptor, 简称fd)是Linux内核所创建的索引,其目的为了高效管理已被打开的“文件”。其实,文件描述符就是一个非负整数(通常是小整数),用于指代被打开的“文件”,而所有对该“文件”的I/O操作都是通过该文件描述符来执行的。
通常,程序刚刚启动时,系统就已经占用了3个文件描述符:0(标准输入)、1(标准输出)和2(标准错误)。如果程序此时打开一个新文件或建立一个socket连接,其对应的文件描述符将会是3。
至于为什么会是3?原因:POSIX标准要求每次打开文件(socket)时,其分配的文件描述符必须是当前进程中最小可用的文件描述符。因此,在网络编程时,网络通信过程中稍不注意就可能造成“串话”现象。
POSIX标准输入输出的文件描述符,如下:
file descriptor | purpose | POSIX | STDIO |
0 | 标准输入 | STDIN_FILENO | stdin |
1 | 标准输出 | STDOUT_FILENO | stdout |
2 | 标准错误 | STDERR_FILENO | stderr |
2.文件描述符、文件指针、打开文件
在Linux系统中,每个文件描述符对应一个打开的文件,同时不同的文件描述符也可以指向同一个文件。相同的文件可以被不同的进程打开,每个进程中有对应的文件描述符(可以相同也可不相同);相同文件也可以在同一个进程中多次打开(对应不同的文件描述符)。上面已简单说明,为了高效管理打开的文件,Linux系统为每个进程维护了一个文件描述表,该表的值都是从0开始的,所以在不同的进城中可以看到相同的文件描述符。这种情况下,相同的文件描述符有可能指向同一个文件,也有可能指向不同的文件,具体情况要依据具体场景分析。要想理解具体的概况,需要了解和查看内核维护的3个数据结构:
- 进程级的文件描述符表
- 系统级的打开文件描述符表
- 文件系统的i-node表
- 控制文件描述符的一组标志(目前,此类标记只定义了一个,即close-on-exec)
- 对应打开文件句柄的应用(文件指针)
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;
}
5.总结
6.参考资料
更多推荐
所有评论(0)