【Linux基础IO】深入Linux文件描述符与重定向:解锁高效IO操作的秘密
📝个人主页🌹:Eternity._
⏩收录专栏⏪:Linux “ 登神长阶 ”
🤡往期回顾🤡:Linux Shell
🌹🌹期待您的关注 🌹🌹
❀Linux基础IO
- 📒1. C语言文件接口
- 🎈写文件
- 🎩读文件
- 📚2. 系统文件I/O
- 🌞open
- 🌄close
- 🏞️write
- ⭐read
- 📜3. 文件描述符fd
- 🍁0 & 1 & 2
- 🍂文件描述符的分配规则
- 📝4. 重定向
- 🌸使用 dup2 系统调用
- 📖5. 总结
前言:在Linux操作系统的广阔世界中,文件描述符(File Descriptor,简称fd)和重定向是理解并高效利用Linux IO(输入/输出)机制的关键基石。它们不仅是系统编程中不可或缺的概念,也是日常命令行操作中的强大工具。掌握这些概念,将使你能够更深入地理解Linux如何管理文件、进程间的通信以及数据的流动,从而编写出更加高效、健壮的应用程序,同时在系统管理和脚本编写中也能游刃有余
文件描述符(fd),简而言之,是Linux内核为了高效管理打开的文件(包括设备、管道等)而引入的一个抽象表示。每个打开的文件或资源都会被分配一个唯一的非负整数作为标识,这个标识就是文件描述符。通过文件描述符,进程可以访问和操作对应的文件或资源,而无需记住复杂的文件名或路径
重定向,则是Linux shell提供的一种强大功能,它允许用户改变标准输入(
stdin
)、标准输出(stdout
)和标准错误输出(stderr
)的默认方向。通过重定向,用户可以将命令的输出直接发送到文件、另一个命令的输入,或者忽略某些输出,从而灵活地控制数据的流向,实现复杂的自动化任务
我将带领大家深入探索Linux文件描述符和重定向的奥秘。我们将从基本概念讲起,逐步深入到它们的内部工作原理、使用技巧以及在实际场景中的应用。通过丰富的示例和详细的解释,读者将能够全面理解并掌握这些核心概念,进而在Linux编程和系统管理中更加得心应手
让我们一起,在Linux基础IO的海洋中扬帆起航,探索更多未知的精彩吧!
📒1. C语言文件接口
C语言提供了丰富的文件操作接口,允许程序以文件的形式对存储设备上的数据进行读写操作。这些接口主要由标准I/O库(stdio.h)中的函数组成,它们为文件的打开、关闭、读写等操作提供了支持,我们在C语言的学习时,已经见识过了,我们来回顾一下
🎈写文件
代码示例 (C语言):
#include <stdio.h>
#include <string.h>
int main()
{
FILE* fp = fopen("log.txt", "w");
if(fp == NULL)
{
perror("open fail");
return 1;
}
// ...... 操作文件
// 将文本内容设置成 "hello world!"
const char *msg = "hello world!\n";
int count = 5;
while(count--)
{
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
return 0;
}
🎩读文件
代码示例 (C语言):
#include <stdio.h>
#include <string.h>
int main()
{
FILE* fp = fopen("log.txt", "r");
if(fp == NULL)
{
perror("open fail");
return 1;
}
char buf[1024];
const char *msg = "hello world!\n";
while(1)
{
size_t s = fread(buf, 1, strlen(msg), fp);
if(s > 0)
{
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp))
{
break;
}
}
fclose(fp);
return 0;
}
对于将信息输出到显示器我们可以使用fprintf,fwrite
代码示例 (C语言):
int main()
{
const char *msg = "hello world\n";
fwrite(msg, strlen(msg), 1, stdout);
fprintf(stdout, "hello world\n");
return 0;
}
stdin & stdout & stderr
- C默认会打开三个输入输出流,分别是
stdin, stdout, stderr
- 这三个流的类型都是
FILE*
, fopen返回值类型,文件指针
文件打开方式
符号 | 作用 |
---|---|
r | 以只读方式打开文件。文件必须存在。 |
w | 以写入方式打开文件。如果文件存在,则覆盖文件(即文件内容会被清空);如果文件不存在,则创建新文件。 |
a | 以追加方式打开文件。如果文件存在,则写入的数据会被添加到文件末尾,而不会覆盖原有内容;如果文件不存在,则创建新文件用于写入。 |
r+ | 以读写方式打开文件。文件必须存在。 |
w+ | 以读写方式打开文件。如果文件存在,则覆盖文件;如果文件不存在,则创建新文件。 |
a+ | 以读写方式打开文件用于追加。如果文件存在,则写入的数据会被添加到文件末尾,文件指针会停留在文件末尾,但允许读取;如果文件不存在,则创建新文件。 |
📚2. 系统文件I/O
系统文件I/O,即系统输入输出操作,是计算机系统中负责数据在内存与外部设备(如磁盘、键盘、显示器等)之间传输的机制。在C语言中,文件I/O操作是一个重要的组成部分,它允许程序读取和写入文件,以及进行其他形式的数据交换
🌞open
在Linux系统编程中,
open
是一个非常重要的系统调用(system call),它用于打开和可能创建文件。这个函数是文件I/O操作的基础,因为它提供了对文件或设备的访问权限,并返回一个文件描述符(file descriptor),该描述符随后可用于其他文件I/O操作
open它有三个参数:
pathname
:要打开或创建的目标文件
flags
:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags
mode
:权限
返回值:
成功
:新打开的文件描述符失败
:-1
open 常用的标志位有:
O_RDONLY,O_WRONLY,O_RDWR,O_CREAT,O_TRUNC
,这些相当于宏,每一个宏只有一个标记位是1的,而且彼此不重复,我们来模拟实现一下
O_RDONLY
: 只读打开O_WRONLY
: 只写打开O_RDWR
: 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT
: 若文件不存在,则创建它。需要使用mode选项,来指明新文的访问权限O_APPEND
: 追加写- 默认情况我们写文件时,是不会将文件清空的,如果我们想要清空就需要加上
O_TRUNC
代码示例 (C语言):
// 每一个宏只有一个标记位是1的,而且彼此不重复
#define Print1 1
#define Print2 (1<<1)
#define Print3 (1<<2)
#define Print4 (1<<3)
void Print(int flags)
{
if(flags&Print1) printf("hello 1\n");
if(flags&Print2) printf("hello 2\n");
if(flags&Print3) printf("hello 3\n");
if(flags&Print4) printf("hello 4\n");
}
int main()
{
Print(Print1);
Print(Print1|Print3);
Print(Print2|Print3|Print4);
Print(Print4);
return 0;
}
在了解完标志位之后,让我们来看看open的用法吧
open代码示例 (C语言):
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT);
if(fd < 0)
{
perror("open");
return 1;
}
// 文件操作
close(fd);
return 0;
}
当我们执行我们的可执行程序后,确实产生了一个文件,但是文件的属性却是乱码,而且文件名还带有颜色,而产生这些的原因,其实是第三个参数,我们可以在创建时,设置权限
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
因为有权限掩码的存在即使我们设置了
666
但是在生成是变成了664
,如果一定要按我们的意思生成,我们可以在代码中屏蔽权限掩码,将掩码设置成0
umask(0);
🌄close
close
函数在不同的编程环境和上下文中可以有不同的具体实现和用途,但通常它用于关闭或释放资源。这些资源可能是文件、网络连接、数据库连接、图形界面中的窗口或任何其他需要显式关闭以避免资源泄露或保持系统整洁的实体
close
函数很简单,重要的是要确保在不再需要资源时调用它,我们只要记住他的头文件和它的用法就可以了
🏞️write
write
函数是在多种编程环境中广泛使用的一个函数,主要用于向文件、网络连接、数据库或其他输出流中写入数据。虽然具体实现和可用的参数可能因编程语言和上下文而异,但write函数的基本用途是相似的
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
const char *msg = "hello world\n";
// 这里计算msg大小时,不需要+1,因此'\0'不是文件所要求的
write(fd, msg, strlen(msg));
close(fd);
return 0;
}
⭐read
read
函数是在多种编程环境中用于从文件、网络连接、内存缓冲区或其他输入流中读取数据的函数。它的具体实现和可用的参数会根据所使用的编程语言、库或上下文而有所不同,但基本的功能和用途是相似的
int main()
{
char buffer[1024];
int fd = open("log.txt",O_WRONLY | O_CREAT);
ssize_t n = read(fd,buffer,sizeof(buffer));
if(n > 0)
{
buffer[n-1] = 0;
}
}
📜3. 文件描述符fd
文件描述符(fd)是File Descriptor的缩写,是Linux等类Unix操作系统中用于表示打开的文件、套接字或其他I/O资源的一个非负整数。文件描述符在操作系统中扮演着重要的角色,它是进程和文件、套接字等资源之间的抽象句柄,通过它可以进行读取、写入、映射或控制等操作
打印文件描述符:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fda = open("loga.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("fda:%d\n", fda);
printf("fdb:%d\n", fdb);
printf("fdc:%d\n", fdc);
printf("fdd:%d\n", fdd);
return 0;
}
我们发现当我们同时打开多个文件时,它们的文件描述符是一个连续的非负整数,相当于数组下标
每一个进程都要知道自己打开了哪些文件,所以进程PCB中会保存一张文件描述符表,文件描述符的本质就是数组的下标
🍁0 & 1 & 2
在POSIX语义中,文件描述符0、1、2被赋予了特殊的含义,分别代表标准输入(STDIN_FILENO)、标准输出(STDOUT_FILENO)和标准错误(STDERR_FILENO)所以我们打开一个文件,它的文件描述符是从3开始
0,1,2对应的物理设备一般是:键盘,显示器,显示器
- 标准输入 键盘 stdin 0
- 标准输出 显示器 stdout 1
- 标准错误 显示器 stderr 2
验证代码:
int main()
{
printf("stdin->fd:%d\n", stdin->_fileno);
printf("stdout->fd:%d\n", stdout->_fileno);
printf("stderr->fd:%d\n", stderr->_fileno);
return 0;
}
文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。
每个进程都有一个指针*files
, 指向一张表files_struct
,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!只要拿着文件描述符,就可以找到对应的文件
操作系统访问一个文件时,只认文件描述符!
🍂文件描述符的分配规则
最小未使用原则:
- 进程在分配文件描述符时,会查询其内部的文件描述符表(内核中的文件指针数组)
- 选择分配最小的、当前未被使用的文件描述符给新打开的文件或流
// 各种头文件
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd:%d\n", fd);
close(fd);
return 0;
}
当我们关闭1之后,我们执行程序发现,并没有在屏幕上产生输入
我们来修改一下代码:
// 各种头文件
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd:%d\n", fd);
printf("stdout:%d\n", stdout->_fileno);
fflush(stdout);
close(fd);
return 0;
}
我们在
刷新之后
发现,本来要打印在屏幕上的fd竟然出现在了log.txt
里面,如果我们先把1关掉,再打开文件,那么给它分配的文件描述符就是1,但是为什么会将内容写到文件里面呢?
- 目前我们打开的文件的文件描述符是1,而printf它只能1,所以打印的内容就被
重定向
到了log.txt中- 为什么是刷新之后有,不刷新就没有呢?-> 因为在没有刷新时,内容是储存在缓冲区的,刷新之后才会出现
📝4. 重定向
在Linux中,重定向是一种将命令的标准输入(stdin)、标准输出(stdout)或标准错误(stderr)重新指向文件或其他命令的技术。这种机制允许用户将命令的输出保存到文件中,或者将文件的内容作为命令的输入。重定向通过使用特定的符号来实现,这些符号主要包括
>、>>、< 和 2>
我们在关闭1后发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向
重定向的本质其实就是修改文件特征fd的下标内容
如果我们将代码中的O_TRUNC修改成O_APPEND
这样代码就变成了追加重定向
追加重定向代码:
// 各种头文件
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd:%d\n", fd);
printf("stdout:%d\n", stdout->_fileno);
fflush(stdout);
close(fd);
return 0;
}
输入重定向代码:
int main()
{
close(0);
int fd = open("log.txt", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
char buf[1024];
fread(buf, 1, sizeof(buf), stdin);
printf("%s\n", buf);
close(fd);
return0;
}
本来fread
是要求我们从键盘输入的,但是他直接从文件里面读取了,那么到底什么是重定向,我们来画图了解一下
重定向 2>
2>
实际上指的是将标准错误(stderr,文件描述符为2)重定向到指定的文件或位置。
默认情况下,命令的标准输出(stdout,文件描述符为1)和标准错误(stderr,文件描述符为2)都会被打印到终端上。通过使用重定向,你可以将这两者的任何一个或两个都重定向到其他地方
int main()
{
fprintf(stdout, "hello stdout\n");
fprintf(stderr, "hello stderr\n");
return 0;
}
我们的指令是将文件标识符为 1 的内容拷贝到log.txt中,但是屏幕还输出了一个 标准错误
但是如果我们想让它们都重定向一个文件里面我们可以用2>
,在以后运用中,我们也可以将1重定向到一个文件中,2重定向到另一个文件中,这样在我们需要时,可以更快速的定位
🌸使用 dup2 系统调用
dup2
是一个系统调用,用于复制一个现有的文件描述符到另一个文件描述符的位置,同时关闭目标文件描述符(如果它之前已打开)。这个调用主要用于重定向标准输入、标准输出或标准错误流到文件或其他I/O设备
dup2代码示例:
int main()
{
//int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd = open("log.txt", O_RDONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
//dup2(fd, 1);
//printf("hello world\n");
//fprintf(stdout, "hello wrold\n");
dup2(fd, 0);
char buf[1024];
fread(buf, 1, sieof(buf), stdin);
printf("%s\n, buf"):
close(fd);
return 0;
}
注释部分代码运行结果
我们可以用自己实现一个简易重定向去添加到我们上一节所编写的简易shell上,由于篇幅太长,我就不展示代码了,感兴趣的可以到我的gitte上去看
在shell增加重定向功能
部分代码展示:
每次重新输入时,为了不被上一次影响,我们需要在main函数中重置redir和filename
的值
📖5. 总结
随着我们对Linux文件描述符(fd)和重定向的深入探讨,我们不仅揭开了这些概念背后复杂而精妙的机制,还见证了它们在实际应用中的广泛与强大。Linux的IO系统,通过文件描述符这一简洁而高效的抽象,使得进程能够灵活地与各种资源交互,都能通过统一的接口进行访问和管理
而重定向,则是Linux shell赋予我们的一柄利剑,它打破了传统IO操作的束缚,让我们能够随心所欲地控制数据的流向。通过重定向,我们不仅可以实现复杂的自动化任务,还能在脚本编写和日常操作中极大地提升效率
然而,学习之路永无止境。Linux的IO系统博大精深,文件描述符和重定向只是其中的冰山一角。未来,我们还将继续探索更多高级话题,如非阻塞IO、异步IO、信号驱动IO等,以进一步拓宽我们的视野和技能边界
让我们携手前行,在Linux的海洋中乘风破浪,共同追寻技术的真谛!
希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!