1. 认识文件描述符
文件描述符的概念:在Linux中,文件描述符是内核为了高效的管理已经被打开的文件所创建的索引 ,它是一个非负整数,用于指代被打开的文件,所有执行I/O操作的系统调用都是通过文件描述符完成的。文件描述符是一个简单的非负整数,用来表明每一个被进程打开的文件。
在Linux中,进程是通过文件描述符 (file descriptors 简称fd)来访问文件的,文件描述符实际上是一个整数。 在程序刚启动的时候,默认有三个文件描述符,分别是:0 (代表标准输入),1 (代表标准输出),2 (代表标准错误)。 我们可以通过open函数得到一个指定文件的文件描述符,如果出现错误则返回-1。open函数需要传入一个文件路径和操作模式,调用会返回一个整型的文件描述符。
在Linux系统中,open函数主要作用就是打开和创建文件,可以根据参数来定制需要的文件的属性和用户权限等各种参数。flag参数相当于是宏,并且是可选的,用于设置打开文件的模式。flag参数的取值如下:
O_RDONLY: 只读模式
O_WRONLY: 只写模式
O_RDWR : 读写模式
O_NONBLOCK: 非阻塞模式
O_APPEND: 追加模式
O_CREAT: 创建并打开一个新文件
O_TRUNC: 打开一个文件并截断它的长度为零(必须有写权限)
O_EXCL: 如果指定的文件存在,返回错误
open的用法
接下来我们打印一些文件描述符,观察一下文件描述符的规律;
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd1 = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd2 = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd3 = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd4 = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd5 = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);printf("fd1: %d\n", fd1);printf("fd2: %d\n", fd2);printf("fd3: %d\n", fd3);printf("fd4: %d\n", fd4);printf("fd5: %d\n", fd5);close(fd1);close(fd2);close(fd3);close(fd4);close(fd5);return 0;
}
可以看到是一串连续的整数,而0,1, 2是默认的文件描述符,分别是0 (代表标准输入),1 (代表标准输出),2 (代表标准错误),所以文件描述符(open对应的返回值)本质就是:数组下标。
当我们打开一个文件,我们并没有创建数组,那么怎么会有上图的数组下标?
在Linux中,打开文件的过程涉及到进程、文件描述符、文件描述表、打开文件表、目录项、索引表之间的联系。当我们打开一个文件时,主要涉及了进程,文件描述符,文件描述表,打开文件表,目录项,索引表之间的联系。
具体来说,当我们打开一个文件时,操作系统会为该进程分配一个文件描述符(file descriptor),并将其存储在该进程的文件描述符表(file descriptor table)中。然后,操作系统会在打开文件表(open file table)中创建一个新的条目,并将该条目与该文件描述符相关联。此外,操作系统还会在目录项(directory entry)和索引表(inode table)中查找该文件,并将其加载到内存中。最后,操作系统会返回该文件描述符给应用程序,以便应用程序可以使用该文件描述符来访问该文件。
打开一个文件,系统做了大致如下几件事,首先CPU会寻找对应的struct task_struct(也就是进程PCB);struct task_struct中有许多的struct file_struct *files指针,这些指针指向的就是不同的文件结构体,而struct file_struct结构体中就记录着文件描述符(操作系统方面,我们必须要访问fd(文件描述符),才能找到文件!);0、1、2对应的是三个默认的文件描述符,之后的文件描述符就是open返回的;磁盘将文件加载到内存,通过fd我们可以查找到内存中的文件,这些大致就是我们访问文件,操作系统的处理。
概念:PCB(Process Control Block)是进程控制块的缩写,是操作系统中最重要的记录型数据结构之一。PCB中记录了操作系统所需要的、用于描述进程情况及控制进程运行所需要的全部信息。
struct file结构体中,包含了文件权限,文件的大小,自己的缓冲区以及readp和writep等信息,这些信息都是以文件的方式存在的,外设通过驱动程序中的代码可以对文件进行读写。
Linux中所有内容都是以文件的形式保存和管理的。普通文件是文件,目录(Windows下称为文件夹)是文件,硬件设备(键盘、监视器、硬盘、打印机)是文件,所以说linux下一切皆文件。
查看文件描述符0, 1, 2,代码如下:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{printf("%d\n", stdin->_fileno);printf("%d\n", stdout->_fileno);printf("%d\n",stderr->_fileno);File* fd = fopen("log.txt", w);printf("%d\n", fd->fileno);return 0;
}
2. 更改默认的文件描述符
文件描述符的分配规则是什么?请看如下代码:
int main()
{//先关闭0,2,再进行打开文件close(0);close(2);int fd1 = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd2 = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd3 = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd4 = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd5 = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);printf("fd1: %d\n", fd1);printf("fd2: %d\n", fd2);printf("fd3: %d\n", fd3);printf("fd4: %d\n", fd4);printf("fd5: %d\n", fd5);close(fd1);close(fd2);close(fd3);close(fd4);close(fd5);return 0;
}
在Linux中,每个进程都有一个独立的文件描述符表,该表是一个数组,每个元素都是一个指向file结构体的指针。当进程打开一个文件时,内核会为该进程分配一个新的文件描述符,并将其返回给进程。文件描述符分配规则是在file_struct数组中,找到当前没有被使用的最小的一个下标, 作为新的文件描述符。
2.1 输出重定向
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{close(1);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); //1printf("aaaaaaaaaaaa\n");printf("aaaaaaaaaaaa\n");printf("aaaaaaaaaaaa\n");//不用关掉fd :close(fd);return 0;
}
如上代码,先关掉默认描述符1(代表标准输出),所以关掉后printf函数就不会打印到显示器上;open后会返回一个文件描述符,根据文件描述符的规则:找到当前没有被使用的最小的一个下标,所以fd就是1;然后再打印字符串。
程序运行后,可以看到,数据没有打印在显示器上,而是打印在log.txt文件中了,这就是输出重定向。重定向的原理:在上层无法感知的情况下,在OS内部,更改进程对应的文件描述符表中,特定下标的指向。
2.2 输入重定向
先在log.txt文件中写入1234 5678
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{close(0);int fd = open("log.txt", O_RDONLY);//0int a = 0, b = 0;scanf("%d %d",&a, &b);printf("a = %d, b = %d\n", a, b);return 0;
}
如上代码,先关掉默认描述符0(代表标准输入),open后会返回一个文件描述符,根据文件描述符的规则:找到当前没有被使用的最小的一个下标,所以fd就是0;然后再读取数据。
可以看到数据不是从键盘上读取的,而是从log.txt文件中读取的,这就是输入重定向。
2.3 追加重定向
追加重定向也是向显示器或文件中打印,所以close(1)。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{close(1);int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);printf("aaaaaaaaaaaa\n");printf("aaaaaaaaaaaa\n");return 0;
}
3. dup2函数
将常规消息打印打log.txt,将异常消息打印到err.txt?由上面概念所示,我们需要close(1),然后open一下,再close(2),然后open一个文件就行。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{close(1);open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);close(2);open("err.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);//因为linux下一切皆文件,所以向显示器打印,本质就是向文件中写入printf("hello printf->stdout\n");printf("hello printf->stdout\n");fprintf(stdout, "hello fprintf->stdout\n");fprintf(stdout, "hello fprintf->stdout\n");fprintf(stderr, "hello fprintf->stderr\n");fprintf(stderr, "hello fprintf->stderr\n");return 0;
}
运行结果如预料一样,如下所示:
如果我们需要将常规消息打印打log.txt,将异常消息打印到err.txt,那么我们要先关闭,再open,这样的工作就比较不方便,所以就有了dup。
dup2()是Linux系统中的一个系统调用,它可以将一个文件描述符复制到另一个文件描述符。 dup2()函数的原型如下:int dup2(int oldfd, int newfd);。
dup2()函数的作用是将newfd指向oldfd指向的文件,如果newfd已经打开,则先关闭newfd,然后将newfd指向oldfd指向的文件。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd1 = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);int fd2 = open("err.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);dup2(fd1, 1);dup2(fd2, 2);//因为linux下一切皆文件,所以向显示器打印,本质就是向文件中写入printf("hello printf->stdout\n");printf("hello printf->stdout\n");fprintf(stdout, "hello fprintf->stdout\n");fprintf(stdout, "hello fprintf->stdout\n");fprintf(stderr, "hello fprintf->stderr\n");fprintf(stderr, "hello fprintf->stderr\n");return 0;
}