感谢阅读East-sunrise学习分享——基础IO——系统IO | 文件描述符fd | 重定向
博主水平有限,如有差错,欢迎斧正感谢有你
码字不易,若有收获,期待你的点赞关注我们一起进步
文件操作对于程序员来说必不可少
C语言有C语言的文件操作接口,JAVA有JAVA的…
所有我们来一波釜底抽薪从根源入手,学习系统IO等知识
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。若打开成功,返回文件指针;若打开失败,返回NULL。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );
写文件
文件输出有许多函数,就不一一赘述,常用的是fprintf
fprintf是将特定的数据格式化到特定的文件流中
读文件
fgets从特定文件流中按行读取到缓冲区中
文件的输入输出,实际上便是在与硬件进行交互;而有三个标准流,会随着计算机系统的开启而默认打开:
IO的意思是:输入(input)和输出(output),具体来说是外部设备和内存之间的输入输出
而上面的三种流属于IO中的外部设备,而在Linux操作系统中,一切皆文件
所以这三个标准流的类型都是 FILE*
系统IO是什么?为何要学?
我们知道,文件存在与磁盘中,而磁盘是硬件,那要访问硬件需要通过谁?——操作系统
所以我们进行文件操作不能绕开OS,因此OS也提供了文件级别的系统调用接口
而我们平时使用的C语言、C++、JAVA…这些上层语言的文件操作接口,这些语言级别的文件操作接口都不相同,但是不论如何,库函数的底层都是调用系统调用接口;也就是说,这些各式语言级别的文件操作接口,其实都是基于系统调用接口去封装而成的。
因此为了降低学习成本,更深刻地了解IO,我们便从最底层的系统IO入手
我们可以通过手册查找具体用法:man 2 open
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
✏️参数说明
pathname: 要打开或创建的目标文件文件名
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值说明
成功:新打开的文件描述符
失败:-1
文件描述符是什么?我们下文再细说
man 2 close
#include
int close(int fd);
1.open接口的第二个参数原理介绍
我们在写程序的过程中,很经常会用到标记位flags。一般用一个整数(0或1)作为标记位,表示某一件事发生了或作为返回值返回。
一个标记位代表一件事,传一个整数;那10个呢?搞10个参数?太麻烦了吧
而我们知道,一个整数是有32个比特位,意味着我们可以通过比特位来传递选项,不同比特位我们自己定义不同的含义
因为我们是按照比特位传递选项,所以势必有要求:1.一个比特位表示一个选项 2.比特位位置不能重复
#include
//每一个宏,对应的数值,只有一个比特位是1,彼此位置不重叠
#define ONE 0x1 //也可写成(1<<0)——... 0000 0001
#define TWO 0x2 //也可写成(1<<1) ——... 0000 0010
#define THREE 0x4 //也可写成(1<<2) —... 0000 0100
#define FOUR 0x8//也可写成(1<<3) —... 0000 1000
void show(int flags)
{
if(flags & ONE) printf("one\n");
if(flags & TWO) printf("two\n");
if(flags & THREE) printf("three\n");
if(flags & FOUR) printf("four\n");
}
int main()
{
show(ONE);
printf("----------\n");
show(TWO);
printf("----------\n");
show(ONE | TWO);
printf("----------\n");
show(ONE | TWO | THREE);
printf("----------\n");
show(ONE | TWO | THREE |FOUR);
printf("----------\n");
return 0;
}
open接口的第二个参数使用原理同理
2.open接口的第三个参数mode
当文件不存在要新建文件时,就需要使用3个参数的接口,使用mode向系统指明要创建文件的权限
否则新建的文件权限说明是乱码
当文件已经存在需要访问文件,便可使用2个参数的函数接口
man 2 write
#include
ssize_t write(int fd, const void* buf, size_t count);
参数:
buf:想写入的缓冲区
count:期望写的字节数
返回值:实际写的字节数(ssize_t是Linux系统定义的有符号整型)
在使用write写入时就需要注意使用两个flags选项
O_TRUNC:打开文件的时候直接清空文件
O_APPEND:追加文件
⭕值得注意的是:
- 写入文件的过程中,不需要写入\0!因为\0是C语言层面上规定的字符串结束标志,可系统IO并不关心这些;系统IO关心的是写入文件的内容,即有效字符即可
- write接口函数的第二个参数是void*,也就是系统IO不会关心你写入的是什么类型的,所以我们要写入什么类型就自己转换成什么再写入
man 2 read
#include
ssize_t read(int fd, void* buf,size_t count);
参数:
buf:读到的内容放入的用户层缓冲区
count:期望读的字节数
返回值:实际读的字节数
⭕值得注意的是:
- 读文件的前提是:文件已经存在,所以读文件时不需要涉及创建及权限的问题,因此调用两个参数的open打开文件即可
- 从函数的原型可发现第二个参数也是void*,说明read函数也没有类型的概念,需要我们自己去准备;比如上面的代码中,我们认为读到的是字符串,所以要提前创建好并且在尾部手动添加\0(0)
在上面的练习中,open函数会有一个返回值fd,称为文件描述符文件描述符是什么?有什么用?我们来一探究竟
我们打开多个文件后打印这些文件的返回值(文件描述符)发现有一些特征
事实上,当我们的系统运行起来时,系统会默认打开三个标准输入输出,因此012其实分别对应的就是标准输入、标准输出、标准错误
那文件描述符为什么是连续小整数?它的本质又是什么呢?
在上文已经有提及到,文件操作的本质:进程和被打开文件的关系
而进程可以打开多个文件,并且系统中也会有许多进程,如此一来,系统中一定会存在大量的被打开的文件而这些大量的被打开的文件需不需要管理呢?-- 肯定需要那管理者是谁呢? – 操作系统(OS)而提到操作系统进行管理,肯定就会想到我们之前反复介绍的操作系统的管理理念——先描述,再组织所以操作系统为了管理对应的文件,必定要为文件创建对应的内核数据结构 ——> struct file { },其中包含了文件的大部分属性
⭕而文件描述符作为一种对文件的标识,也是文件的属性之一,所以文件描述符fd会在struct file { }中
话又说回来,012对应的标准输入输出的类型是FILE*,之前并没有对此类型进行了解,而现在学习至此
知道了操作系统为了管理,会给每个文件创建一个struct file之后再组织管理起来
但是打开的这么多文件,进程又如何知道哪个文件是它的呢?所以为了让进程和文件能够构建联系,操作系统创建了一个结构体struct files_struct来构建文件和进程之间关系,这个结构中又包含了一个数组struct file* fd_array[ ],也就是一个指针数组,进程每新打开一个文件,文件的地址便会填到此指针数组中;而每个进程的PCB里面都也保存了一个指针,这个指针指向了那个属于此进程的数据结构对象。
所以现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了struct file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张files_struct(文件描述符表),该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
这也解释了为什么write和read这样的系统调用接口一定要传入文件描述符fd,执行系统调用接口是进程执行的,通过进程PCB,找到自己打开的文件描述符表,再通过fd索引数组找到对应的文件,从而对文件进行操作✔️
✅结论:文件描述符fd本质上就是:进程与被打开文件之间维持关系的数组的下标
✏️直接看代码
#include
#include
#include
#include
int main()
{
int fd = open("log.txt", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
输出发现是fd:3
那关闭0或者2呢?
#include
#include
#include
#include
int main()
{
close(0);
//close(2);
int fd = open("log.txt", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
发现结果是:fd:0或者fd:2
可见,文件描述符的分配规则:在fifiles_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
那如果关闭1呢?看代码:
#include
#include
#include
#include
#include
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY|O_CREAT, 00644);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件log.txt当中,其中,fd=1。这种现象叫做输出重定向。
⭕常见的重定向有,> : 输出重定向 >> :追加重定向 < :输入重定向
所以重定向的本质是:上层用的fd不变,在内核中更改fd对应的struct file*的地址
为了支持重定向,系统也有支持重定向的接口,其作用是对文件描述符下标里面的内容进行拷贝
函数原型:
#include
int dup2(int oldfd, int newfd);
dup2的文档解释:
makes newfd be the copy of oldfd, closing newfd first if necessary
翻译来说就是:newfd是oldfd的一份拷贝
所以假设,输出重定向,其操作是要让文件描述符1中的地址不再指向显示器文件,而是指向log.txt;也就是要将log.txt的地址拷贝到1中
因此在调用dup2时即是:dup2(fd,1);
♨️输出重定向
int main()
{
int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
assert(fd > 0);
dup2(fd, 1);
printf("open fd: %d\n", fd); //printf --> 默认stdout
fprintf(stdout, "open fd: %d\n", fd); // =printf
fflush(stdout);
close(fd);
return 0;
}
注意,系统层面,open打开文件时带了选项O_TRUNC,使得每次打开文件都会清空原来的内容。而在C语言中打开文件的“w”选项,也会使得把原始文件清空,说明上层封装了这个选项
♨️追加重定向
只需在输出重定向的基础上,在打开文件时把O_TRUNC选项改为O_APPEND选项即可
♨️输入重定向
int main()
{
int fd = open("log.txt", O_RDONLY);
//输入重定向的前提时文件需存在
assert(fd > 0);
dup2(fd, 0);
char line[64];
while(1)
{
if(fgets(line, sizeof(line), stdin) == NULL) beak;
printf("%s",line);
}
close(fd);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#define NONE_REDIR 0 //没有重定向
#define INPUT_REDIR 1 //输入重定向
#define OUTPUT_REDIR 2 //输出重定向
#define APPEND_REDIR 3 //追加重定向
#define trimSpace(start) do{\
while(isspace(*start)) ++start;\
}while(0)
int redirType = NONE_REDIR;
char* redirFile = NULL;
//"ls -a -l -i > log.txt" ---> "ls -a -l -i" "log.txt"
void commandCheck(char* commands)
{
assert(commands);
char* start = commands;
char* end = commands + strlen(commands);
//遍历寻找重定向符号
while (start < end)
{
if (*start == '>')
{
*start = '\0';
++start;
redirType = OUTPUT_REDIR;
if (*start == '>')
{
*start = '\0';
++start;
redirType = APPEND_REDIR;
}
//消除空格
trimSpace(start);
redirFile = start;
break;
}
else if (*start == '<')
{
*start = '\0';
++start;
redirType = INPUT_REDIR;
trimSpace(start);
redirFile = start;
break;
}
else
{
++start;
}
}
}
int main()
{
while (1)
{
//每次都要重置一次
redirType = NONE_REDIR;
redirFile = NULL;
//...
//上篇博客myshell的内容
//...
commandCheck(lineCommand);
//...
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
switch (redirType)
{
case NONE_REDIR:
//什么都不做
break;
case INPUT_REDIR:
{
int fd = open(redirFile, O_RDONLY);
if (fd < 0)
{
perror("open");
exit(-1);
}
//重定向的文件已经成功打开
dup2(fd, 0);
}
break;
case OUTPUT_REDIR:
case APPEND_REDIR:
{
int flags = O_WRONLY | O_CREAT;
if (redirType == APPEND_REDIR)
flags |= O_APPEND;
else
flags |= O_TRUNC;
int fd = open(redirFile, flags, 0666);
if (fd < 0)
{
perror("open");
exit(-1);
}
dup2(fd, 1);
}
break;
default:
printf("bug?\n");
break;
}
}
}
}
一切皆文件时Linux的设计哲学,体现在操作系统的软件设计层面
在计算机中,像键盘、显示器、磁盘、网卡等硬件称为外设,外设的任何数据要进行处理都得先读到内存中,然后再刷新到其他外设中,这便是IO的过程
我们知道,操作系统为了管理软硬件资源,所以对每个硬件都进行了描述,所以每个设备都有其对应的内核结构体对其描述。而不同的硬件之间都有其不同的读写方法,如果无法读/写,方法可以为空,但是每个硬件都统一拥有IO读写方法,存在于各种硬件匹配的驱动程序之中
每种硬件的访问方法一定是不一样的,那又是如何做到一切皆文件呢?
事物之间,各不相同却又大有相同为了便于使用和管理,此时面向对象的思想便诞生了…
但是Linux是C语言写的,如何用C语言实现面向对象,甚至多态呢?
虽然每个硬件的具体属性不同(比如此硬件的存储情况、数据的读取状态进度…),但是我们遵循面向对象的思想,将这些硬件的各种属性抽象出来,统一起来✅就好比,鸡鸭鹅肯定是不同种生物吧,但是我们可以将他们统一看成“动物类”,而这些鸡鸭鹅便是动物类实例化出来的各种对象
所以Linux在设计时,还设计了struct file结构体,里面包含了每个底层硬件的属性,在这个结构体之中定义了许多属性,便能将各种硬件都笼统起来
struct file
{
int size;
mode_t mode;
int user;
int group;
......
//函数指针
int (*readp)(int fd, void* buffer, int len);
int (*writep)(int fd, void* buffer, int len);
......
}
为了实现一切皆文件,Linux做了软件的虚拟层vfs(虚拟文件系统),会统一维护每一个打开文件的结构体struct file;每个设备都有其对应的结构体对象,里面包含了各种定义、函数指针…而这一切都是在操作系统里面维护的
在struct file上层压根就不关心你底层的每个硬件之间具体的不同的读写方法,他只看到了操作系统维护的struct file,所有文件都是调用统一的接口
写在最后 我们今天的学习分享之旅就到此结束了
感谢能耐心地阅读到此
码字不易,感谢三连
关注博主,我们一起学习、一起进步