Linux文件描述符
在Linux系统中几乎所有可读写的对象都可视为文件,比如标准输入输出、磁盘文件、套接字、管道等,都可以用read
函数读取数据,用write
函数写入数据。 这一点相对于Windows系统来说更加统一,在Windows系统中上面列举的每个对象都有其专门的一组函数读写。
下面分别描述这几种文件对象。
标准输入输出
在C语言中,printf
向控制台窗口输出信息,scanf
从控制台窗口输入信息,并且内置了stdin、stdout、stderr
3个文件描述符,分别对应数字0、1、2。
#define stdin (&__iob_func()[0])
#define stdout (&__iob_func()[1])
#define stderr (&__iob_func()[2])
在C++语言中,cout
向控制台输出,cin
从控制台输入,cerr
错误输出。
在Linux系统头文件unistd.h
定义了类似的3个宏。
STDERR_FILENO
File number of stderr; 2.
STDIN_FILENO
File number of stdin; 0.
STDOUT_FILENO
File number of stdout; 1.
每当我们在shell里运行一个新程序时,shell进程都会为新程序打开这3个描述符,并且指向shell终端。 所以标准输出、标准错误输出在shell上输出,标准输入在shell上输入。
当然我们也可以修改这3个描述符的指向,比如将标准输出指向一个文件,那么在程序里printf、cout输出的内容将不在shell上显示,而是存入了文件里,常见的命令比如:
./a.out > aaa.txt
将标准输出存入aaa.txt文件中。
磁盘文件
在C语言里使用fopen
打开文件,fread
读取文件,fwrite
写入文件,fclose
关闭文件。
在C++语言里有ifstream
读取文件,ofstream
写入文件。但通常更习惯于使用C语言提供的接口读写文件。
在Linux系统api里提供了open
打开文件,read
读取文件,write
写入文件,close
关闭文件。实际上C和C++库函数最终调用了系统api函数完成真正的读写功能。
套接字
套接字用于网络数据的读写,套接字相关函数操作的对象都是socket套接字,Linux提供了socket
函数用于创建一个socket对象。
管道
管道是一种进程间通信接口,既可用于两个不同进程间通信,也可以用于同一个进程内部通信,Linux提供了pipe
函数用于创建一个管道。
描述符分配规则
下面一段代码(fd.c)输出了描述符具体的数值,用于分析文件描述符的分配规则。
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main()
{
/*标准输入、输出*/
printf("stdin = %d\n", STDIN_FILENO);
printf("stdout = %d\n", STDOUT_FILENO);
printf("stderr = %d\n\n", STDERR_FILENO);
/*打开两个磁盘文件*/
int fd1 = open("./a1.txt", O_RDONLY);
int fd2 = open("./a2.txt", O_RDONLY);
printf("fd1 = %d\n", fd1);
printf("fd2 = %d\n\n", fd2);
/*创建两个套接字*/
int sfd1 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
int sfd2 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
printf("sfd1 = %d\n", sfd1);
printf("sfd2 = %d\n\n", sfd2);
/*关闭之前打开的两个磁盘文件*/
printf("close fd1 fd2\n");
close(fd1);
close(fd2);
/*创建一个管道*/
int pipefd[2];
pipe(pipefd);
printf("pipefd1 = %d\n", pipefd[0]);
printf("pipefd2 = %d\n\n", pipefd[1]);
/*再次打开刚才那两个磁盘文件*/
int fd3 = open("./a1.txt", O_RDONLY);
int fd4 = open("./a2.txt", O_RDONLY);
printf("fd3 = %d\n", fd3);
printf("fd4 = %d\n\n", fd4);
getchar();
return 0;
}
运行结果:
stdin = 0
stdout = 1
stderr = 2
fd1 = 3
fd2 = 4
sfd1 = 5
sfd2 = 6
close fd1 fd2
pipefd1 = 3
pipefd2 = 4
fd3 = 7
fd4 = 8
代码分析:
(1)启动程序后,首先测试标准输入、输出、错误输出的fd(File descriptor,简称fd)值,分别为0、1、2。
(2)然后测试打开磁盘文件a1.txt、a2.txt后返回的fd值,若没有这个文件,可先使用命令在测试代码目录下创建两个空文件
touch a1.txt a2.txt
测试结果显示,打开两个磁盘文件返回的fd为3、4,因为0、1、2已经被标准输入输出占用。
(3)再测试套接字的fd,创建两个套接字对象,返回的fd分别为5、6。
(4)关闭第2步打开的两个磁盘文件fd。
(5)再测试管道的fd,创建一个管道,返回的两个fd分别为3、4,这说明已经关闭的fd可再次使用。
(6)再次打开那两个磁盘文件,返回的fd分别为7、8。
分配规则总结:
(1)Linux 系统里标准输入输出、磁盘文件、套接字、管道共用一套fd编号。
(2)fd编号总是从0开始逐步递增。
(3)若某个已使用的fd被关闭释放后,可以被再次使用。
另外我们可以在/proc/(进程id)/fd目录下更直观的看到进程fd状态。
由于Linux系统将多种读写对象统一抽象为文件类型,并提供一致的接口,因此在实际开发中我们可以更为灵活的处理文件对象。
比如select/epoll等函数一般用于检测socket的读写状态,但我们也可以将其用于检测标准输入输出、管道的读写状态,这就是本文分析文件描述符的意义。