深度理解
文件描述符的实质:文件描述符是内核为每个进程维护的一个打开文件记录表的索引值
C语言如何访问系统? 就是通过文件描述符;同样的C++的cin、cout等类中也必须有文件描述符!没有文件描述符,怎么通过操作系统访问(系统调用)外设呢! 每个编程语言都是如此!
通过上述的引出,我们可以知道文件描述符的实质是:
文件描述符是一个非负整数,用于标识不同的已打开文件。
文件描述符是内核为了高效管理已打开文件所创建的索引,它可以用来调用各种I/O系统调用函数。
文件描述符是进程级别的,每个进程都有自己独立的一组文件描述符,并且默认有三个预定义的描述符:0代表标准输入,1代表标准输出,2代表标准错误输出。
文件描述符可以被复制、重定向、关闭等操作,但不能被直接读写。要读写一个已打开文件,需要使用read、write等系统调用函数,并传入相应的文件描述符作为参数。
文件描述符表和file结构体之间的关系是:
文件描述符表是内核用来存储每个进程的文件描述符和对应的打开文件信息的表格。每个进程在其进程控制块(PCB)中都保存着一份文件描述符表
file结构体是内核用来表示已打开文件的数据结构,它包含了当前读写位置、访问模式、状态标志等信息,以及指向对应inode对象或者i-node表项的指针。file结构体也可以称为打开文件句柄或者打开文件表项。
文件描述符表和file结构体之间通过指针相互连接,一个文件描述符可以指向一个或多个file结构体,一个file结构体也可以被一个或多个文件描述符所指向。这样可以实现不同进程或同一进程中不同文件描述符共享同一个已打开文件。
文件描述符的分配规则:系统在创建文件描述符时会寻找当前未使用的最小下标
关闭012文件描述符产生的现象(新打开文件的fd被赋值为0或1或2)
当关闭0或2时,打印出来的log.txt对应的fd的值就是对应的关闭的0或2的值,而当关闭1时,显示器不会显示对应的fd的值。
1 #include <stdio.h> 2 #include <sys/types.h> 3 #include <sys/stat.h> 4 #include <fcntl.h> 5 #include <unistd.h> 6 7 int main() 8 { 9 //close(0); 10 //close(1); 11 //close(2); 12 umask(0000); 13 int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//没有指明文件路径,默认在当前路径下,也就是当前进程的工作目录 14 if(fd<0) 15 { 16 perror("open"); 17 return 1; 18 } 19 20 printf("open fd:%d\n",fd); 21 close(fd); 22 return 0; 23 }
测试结果:
分析:
所以实际上文件描述符在分配时,会从文件描述符表中的指针数组中,从小到大按照顺序找最小的且没有被占用的fd来进行分配,自然而然关闭0时,0对应存储的地址就会由stdin改为新打开的文件的地址,所以打印新的文件的fd值时,就会出现0。
关闭2也是这个道理,fd为2对应的存储的地址会由stderr改为新打开的文件的地址,所以在打印fd时,也就会出现2了。
文件描述符的分配规则是:
当一个进程打开一个新的文件时,系统会在该进程的文件描述符表中寻找当前未使用的最小下标,并将其分配给该文件。
当一个进程关闭一个已打开的文件时,系统会将该文件对应的文件描述符表项置为空,并释放其占用的资源。
当一个进程复制或重定向一个已打开的文件时,系统会在该进程或目标进程的文件描述符表中寻找当前未使用的最小下标,并将其指向同一个file结构体。
下面是一些示例代码:
- 打开一个新的文件:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/stat.h> int main() { // 打开或创建一个新文件 int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); if (fd == -1) { // 打开失败,打印错误信息并退出 perror("open error"); exit(1); } // 打开成功,打印分配到的文档描述符 printf("open success, fd = %d\n", fd); // 关闭文档 close(fd); return 0; }
- 关闭一个已打开的文档:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/stat.h> int main() { // 打开或创建一个新文档 int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); if (fd == -1) { // 打开失败,打印错误信息并退出 perror("open error"); exit(1); } // 打开成功,打印分配到的文档描述符 printf("open success, fd = %d\n", fd); // 关闭文档 int ret = close(fd); if (ret == -1) { // 关闭失败,打印错误信息并退出 perror("close error"); exit(1); } // 关闭成功,打印关闭信息 printf("close success\n"); return 0; }
文件重定向与dup2系统调用 (内核中更改fd对应的struct file*地址)
重定向命令>和>>的含义和用法
重定向命令>和>>都是用来将一个命令的标准输出或错误输出重定向到一个文件中,而不是显示在屏幕上。
重定向命令>表示覆盖模式,即如果目标文件已经存在,那么原来的内容会被清空,然后写入新的内容。例如:echo “hello” > log.txt 表示将字符串"hello"写入到log.txt文件中,如果log.txt文件已经存在,那么原来的内容会被覆盖。
重定向命令>>表示追加模式,即如果目标文件已经存在,那么新的内容会被追加到原来的内容后面。例如:echo “world” >> log.txt 表示将字符串"world"追加到log.txt文件中,如果log.txt文件已经存在,那么原来的内容会保留。
重定向命令>和>>可以指定不同的文件描述符来重定向不同类型的输出。默认情况下,如果不指定文件描述符,那么就是1,表示标准输出。如果要重定向错误输出,就要指定2作为文件描述符。例如:ls -l /etc/passwd /etc/abc > log1.txt 2> log2.txt 表示将ls -l 命令的标准输出重定向到log1.txt文件中,并将错误输出重定向到log2.txt文件中。
测试如下:
我们vim一个abc文件,可以看到abc文件中的内容如下图所示:
然后我们使用 ls > abc 命令之后可以看到abc中的内容已经被清空重定向了
:追加重定向,直接在文件的尾部进行重定向
我们对abc文件进行追加重定向,可以看到,直接在文件的尾巴进行了重定向
重定向原理
简单说将 fd_array 数组当中的元素struct file* 指针的指向关系进行修改,改变成为其它的struct file结构体的地址—每个文件描述符都是一个内核中文件描述信息数组的下标,对应有一个文件的描述信息用于操作文件,而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件
详细说文件操作重定向的原理是通过改变文件描述符对应的文件描述信息,从而实现改变所操作的文件。文件描述符是一个整数,表示进程和被打开文件的关系,通常有标准输入(0)、标准输出(1)和标准错误(2)三种。重定向可以分为输出重定向、追加重定向和输入重定向三种类型。输出重定向是将本应该打印到显示器的内容输出到了指定的文件中,例如 ls > list.txt;追加重定向是将本应该打印到显示器的内容追加式地输出到了指定的文件中,例如 ls >> list.txt;输入重定向是将本应该从键盘中读取的内容改为从指定的文件中读取,例如 cat < input.txt。在Linux系统中,可以使用dup2系统调用来实现重定向,它可以将一个文件描述符复制到另一个文件描述符,并关闭后者。
dup2函数的功能和参数含义
int dup2(int oldfd, int newfd);
其实这个函数挺绕的,要理解起来需要自己研究一下才行:
通过man手册我们可以对dup2函数进行一些了解:
int dup2(int oldfd, int newfd);
函数功能为将newfd描述符重定向到oldfd描述符,相当于重定向完毕后都是操作oldfd所操作的文件
但是在过程中如果newfd本身已经有对应打开的文件信息,则会先关闭文件后再重定向(否则会资源泄露)
怎么用?怎么传参数?— 拷贝的整数所表示的内容 — 注意最后只有oldfd保留就可以了!! 就是oldfd把newfd覆盖了 — 一般传参例如:fd 1 (重定向输出)— 只保留了fd
所以dup2函数是一个用于复制文件描述符的系统调用,它的功能是将参数oldfd所指的文件描述符复制到参数newfd所指定的数值,如果newfd已经被打开,则先关闭它,如果newfd等于oldfd,则不做任何操作。dup2函数返回新的文件描述符,或者在出错时返回-1。
输出重定向:从原来的输出到屏幕改为输出到文件中,这就叫做输出重定向。
而追加重定向的方式也比较简单,只要将文件打开方式中的O_TRUNC替换为O_APPEND即可。
8 int main() 9 { 10 umask(0000); 11 int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//输出重定向 E> 12 int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);//追加重定向 13 if(fd<0) 14 { 15 perror("open"); 16 return 1; 17 } 18 19 dup2(fd,1); 20 21 printf("open fd:%d\n",fd);// printf --> stdout 22 fprintf(stdout,"open fd:%d\n",fd);// fprintf --> stdout 23 24 const char* msg = "hello linux"; 25 write(1,msg,strlen(msg));//向显示器上write 26 27 close(fd); 28 return 0; 29 }
输出重定向:从原来的键盘中读取数据,改为从文件fd中读取数据,这就叫做输入重定向。
文件log.txt中的内容,作为输入重定向重新输出到显示器中,即使fgets获取的方式是stdin也没有关系,因为我们使用dup2将stdin中的地址改为了文件log.txt的地址
8 int main() 9 { 10 umask(0000); 13 int fd = open("log.txt",O_RDONLY);//输入重定向 14 if(fd<0) 15 { 16 perror("open"); 17 return 1; 18 } 19 20 dup2(fd,0);//由键盘读取改为从fd文件中读取 21 char line[64]; 22 while(1) 23 { 24 printf("<"); 25 if(fgets(line,sizeof(line),stdin)==NULL) break; 26 printf("%s",line); 27 } 28 }
示例:
void func() { int fd = open("./tmp.txt", O_RDWR|O_CREAT, 0664);//打开文件 if (fd < 0) { return -1; } //将标准输出,重定向到文件,这样则写往标准输出的数据会被写入到文件中,而不是被打印 dup2(fd, 1); //printf内部操作的是stdout标准输出文件流指针,而文件流指针本质上内部包含的是1号描述符成员 //printf的打印就是向标准输出写入数据,因为标准输出已经被重定向,因此数据会被写入文件中,而不是直接打印 printf("hello bit"); return 0; }
Linux下面一切皆文件!
不同的硬件的读写方法一定是不一样的,但在OS看来,一切设备和文件都是struct file内核数据结构,在管理对应的硬件时,虽然硬件的管理方法不在OS层,而是在驱动层,这也没有关系,只需要利用struct file结构体中的函数指针,调用对应的硬件的读写方法即可。
终究还是封装的思想!
Linux下一切皆文件是指,Linux系统中的所有资源,无论是硬件设备、普通文件、目录、进程、网络连接等,都可以被抽象为文件,并且可以使用统一的接口来访问和操作。
这样做的好处是,简化了开发者和用户对不同资源的处理方式,提高了系统的灵活性和可扩展性。
这样做的不利之处是,需要在文件系统中挂载每个硬件设备才能使用它们,而且可能会造成一些性能损失。