基础I/O
目录
一.理解“文件”
1.1狭义理解
1.2广义理解
1.3文件操作的归类认知
1.4系统角度
二.回顾C文件接口
2.1myfile.c写文件
2.2myfile.c读文件
2.3简单实现cat命令
2.4输出信息到显示器,你有哪些方法?
2.5 stdin & stdout & stderr
标准输入(stdin)
标准输出(stdout)
标准错误(stderr)
2.6打开文件的方式
2.6.1文件打开模式
2.6.2相关文件指针操作函数的复习
三.系统文件I/O
3.1一种传递标志位的方法
3.2myfile.c写文件
3.3myfile.c读文件
3.4文件描述符
3.4.1 0 & 1 & 2
3.4.2什么是文件描述符
3.4.3文件描述符分配原则
3.4.4重定向原理
3.4.5自主Shell新增功能
我们真正理解文件吗???
我们打开文件,读文件,写文件这些文件操作是比较肤浅的,我们刚开始学习文件的时候,最多也就在语言层面上,能够把文件部分大概用一下,我们只有将Linux的系统级别的文件搞清楚了,那么很多东西才能慢慢理解,所以现阶段我们对文件的理解最多也就是操作,但是我们本篇就是为了帮助我们解决文件的操作,真正的理解文件。
在我们开始谈文件之前,我们要输出一个结论:
我们以一个问题为切入点:如果在磁盘上新建一个文件,文件的大小为0,那么这个文件在磁盘上是否要占据空间???
答案是:要的!!!因为文件=内容+属性,没有内容,但是至少有文件名,文件权限等的属性。比如说我们之前学到的" ls -l"就是查看文件的属性,"nano/vim"就是访问文件,修改文件的内容,比如我们对文件进行操作的时候," cat "来打印文件的内容,下面,我们来理解一下文件。
一.理解“文件”
1.1狭义理解
- ⽂件在磁盘⾥,而磁盘本质上是一个外设,所以我们访问文件,本质就是在系统和外设之间进行I/O(读写磁盘);
- 磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的,和内存不一样,我们将电脑电源拔了,那么当前的进程,占用的内存大小就直接没了,但是保存的文件对应的数据会一直存在(因为文件在磁盘上);
- 磁盘是外设(即是输出设备也是输⼊设备);
- 磁盘上的⽂件本质是对⽂件的所有操作,都是对外设的输⼊和输出:简称 IO。
1.2广义理解
/dev
目录)进行抽象,而软件程序则以可执行文件或脚本文件的形式存储在文件系统中。这种“一切皆文件”的设计理念不仅简化了系统设计,还为用户和开发人员提供了统一、灵活的接口。
1.3文件操作的归类认知
- 对于 0KB 的空⽂件是占⽤磁盘空间的;
- ⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件 = 属性(元数据)+ 内容);
- 所有的⽂件操作本质是⽂件内容操作和⽂件属性操作。
1.4系统角度
- 对⽂件的操作本质是进程对⽂件的操作
- 磁盘的管理者是操作系统
- ⽂件的读写本质不是通过 C 语⾔ / C++ 的库函数来操作的(这些库函数只是为⽤⼾提供⽅便),⽽是通过⽂件相关的系统调⽤接⼝来实现的
访问文件,第一件事是要打开文件!在编程中,无论是使用 C 语言的 fopen()
,还是 C++ 的文件流,或者是其他语言的文件操作方法,访问文件的第一步都是打开文件。这意味着在程序运行时,通过调用特定的函数(如 fopen()
)来建立对文件的访问权限。然而,这个过程仅在程序实际运行时才会发生。
也就是:在程序运行之前,文件不会被打开。只有当程序实际运行时,代码中的文件操作函数(如fopen()
)才会执行,从而在内存中打开文件。
操作系统加载你的程序(my_program
)到内存中。程序开始执行,到达 fopen()
函数。此时,fopen()
函数被调用,程序尝试打开文件 example.txt
。如果文件存在且权限正确,文件被成功打开,程序继续执行。如果文件不存在或权限不足,fopen()
返回 NULL
,程序会报错并退出。
也就是进程打开的文件!!!,所以对文件的操作,本质是进程对文件的操作,访问文件的本质其实是进程在访问文件!!!
我们在学习冯·诺依曼体结构,进程管理的时候,我们清楚:磁盘是一个硬件,而我们操作系统是要对硬件进行先描述再组织的管理的。所以操作系统才是磁盘真正的管理者,访问文件本质就是访问磁盘,所以,我们对文件进行访问,我们是通过C/C++语言来访问,我们通过库函数(fopen,fclose......),但是,我们心里应该十分清楚,因为磁盘的管理者其实是OS,所以只有OS才能有资格来真正的去访问对应的硬件,控制对应的硬件。所以不管我们学了多少文件操作的方式,操作系统在底层一定为我们提供了对应的文件级别的系统调用接口,让我们访问对应的文件。
也就是说:fopen,fclose....这些库函数封装了底层OS的文件系统调用!!!
可是把文件打开这是一个非常抽象的行为,什么叫做把文件打开呢?我们举个例子:今天我的一个进程,他可不可以打开10个文件,这是可以的,之前说过,我们程序运行,默认打开有三个文件:标准输入,标准输出和标准错误。一个进程是一定可以打开多个文件的。所以系统当中,如果存在50个进程,有没有可能存在100个已经被打开的文件?
系统当中会同时存在大量的文件,有的文件被打开了,准备关闭,有的文件刚被打开,有的文件被打开了是正在被某个进程所访问的,而我们实际上要打开对应的文件是一定要有由OS去打开的,因为必须由OS从磁盘里把文件读到内存里,我们这时候才能够访问文件,A进程对应3个文件,B进程对应4个文件....所以进程与被打开文件的对应关系,系统也要维护起来。
说这么多,就想告诉大家:操作系统层面上,如果文件被打开了,那么在操作系统内,可能会同时存在非常多的文件,操作系统就应该把这些被打开的文件管理起来。
所以怎么管理呢?
先描述,再组织!!!(转化成链表的增删查改)
所以我们学习文件的本质就是学习进程访问文件,进程是个数据结构对象+代码和数据,而文件也是一个内核级的数据结构对象,所以进程和文件之间的关系,最终转化成了两种内核数据结构之间的关系,就是一个结构体对象和另一个结构体对象之间的关系!!!
从更宏观的角度来看,文件确实可以分为两大类:内存级文件(被打开的文件)和磁盘级文件(存储在磁盘上的文件)。这种分类有助于更好地理解文件在系统中的存在形式和访问方式。
我们本篇重点会放在内存级文件上!!!
我们在文件系统专题再重点谈论磁盘级文件。
我们下面讲的所有操作理论都是基于文件即将被打开,已经被打开,属于内存级文件方面的知识!
二.回顾C文件接口
2.1myfile.c写文件
#include <stdio.h>
#include <string.h>
int main()
{
//没有就新建
//覆盖式写入
FILE *fp = fopen("log.txt", "w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
const char *msg = "hello world: ";
int cnt = 1;
while(cnt <= 10)
{
char buffer[1024];
snprintf(buffer,sizeof(buffer),"%s%d\n",msg,cnt++);
//往文件里写入
fwrite(buffer,strlen(buffer),1,fp);
}
fclose(fp);
return 0;
}
fwrite
本身是一个写入数据的函数,其是否覆盖文件内容取决于文件打开模式:(具体看2.6)
-
如果文件以
"w"
或"wb"
模式打开,fwrite
会覆盖文件的全部内容。 -
如果文件以
"r+"
或"r+b"
模式打开,fwrite
会从文件指针当前位置开始覆盖数据。 -
如果文件以
"a"
或"ab"
模式打开,fwrite
会将数据追加到文件末尾,不会覆盖原有内容。
因此,fwrite
是否覆盖文件内容,取决于文件的打开模式和文件指针的位置。
2.2myfile.c读文件
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "r"); // 尝试以只读模式打开文件
if(!fp){ // 如果文件指针为空,说明打开文件失败
printf("fopen error!\n");
return 1; // 返回错误码 1
}
char buf[1024]; // 定义一个字符数组,用于存储从文件中读取的数据
const char *msg = "hello bit!\n"; // 定义一个字符串,程序中未使用到
while(1){ // 无限循环,用于持续读取文件直到结束
ssize_t s = fread(buf, 1, strlen(msg), fp); // 从文件中读取数据到buf,每次读取strlen(msg)个字节
if(s > 0){ // 如果读取到数据
buf[s] = 0; // 在buf的末尾添加字符串结束符'\0',确保字符串正确终止
printf("%s", buf); // 打印读取到的数据
}
if(feof(fp)){ // 检查是否到达文件末尾
break; // 如果到达文件末尾,跳出循环
}
}
fclose(fp); // 关闭文件
return 0; // 程序正常结束
}
2.3简单实现cat命令
cat命令就是打印文件内容:
#include <stdio.h>
#include <string.h>
//cat myfile.txt
int main(int argc, char *argv[])
{
if(argc != 2)
{
printf("Usage: %s filename]n",argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "r");
if(fp ==NULL)
{
perror("fopen");
return 2;
}
while(1)
{
char buffer[256];
memset(buffer,0,sizeof(buffer));
int n = fread(buffer,1,sizeof(buffer),fp);
if(n > 0)
{
printf("%s",buffer);
}
if(feof(fp))
{
break;
}
}
fclose(fp);
return 0;
}
效果:
2.4输出信息到显示器,你有哪些方法?
我们有纯C语言的方法:
man 3 printf
#include <stdio.h>
#include <string.h>
int main()
{
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
const char *msg = "hello fwrite\n";
fwrite(msg,strlen(msg),1,stdout);
return 0;
}
当我们向显示器打印,本质上就是向显示器文件写入,因为Linux下,一切皆文件。
2.5 stdin & stdout & stderr
标准输入(stdin)
-
文件描述符:0。
-
用途:标准输入是程序接收输入数据的主要途径,通常与键盘关联。例如,在命令行中输入命令时,键盘输入的数据会通过标准输入流传递给程序。(键盘文件)
-
重定向:可以通过
<
符号将输入重定向为来自文件的内容。例如:command < inputfile
。
标准输出(stdout)
-
文件描述符:1。
-
用途:标准输出是程序输出数据(如计算结果或状态信息)的默认目的地,通常与终端屏幕关联。例如,使用
echo
命令输出的内容会显示在屏幕上。(显示器文件) -
重定向:可以通过
>
符号将输出重定向到文件。例如:command > outputfile
。还可以使用>>
将输出追加到文件。
标准错误(stderr)
-
文件描述符:2。
-
用途:标准错误是程序写入错误信息的通道。即使标准输出被重定向,标准错误仍默认输出到屏幕上。这使得错误信息可以被单独捕获和处理。(显示器文件)
-
重定向:可以通过
2>
符号将错误信息重定向到文件。例如:command 2> errorfile
。还可以将标准错误和标准输出同时重定向到同一个文件,例如:command > outfile 2>&1
。
总结:
-
stdin:默认从键盘接收输入,文件描述符为 0。
-
stdout:默认输出到屏幕,文件描述符为 1。
-
stderr:默认输出错误信息到屏幕,文件描述符为 2。
我们程序是做数据处理的!!! 对于数据处理程序而言,stdin
、stdout
和 stderr
默认打开的设计极大地简化了数据的输入输出流程,使得程序能够直接从标准输入读取数据并输出结果,同时支持灵活的重定向和管道操作,方便与其他程序组合使用。此外,独立的错误输出流 stderr
有助于清晰地记录和区分错误信息,而不干扰正常的处理结果,从而提高了程序的可维护性和用户体验。这种设计不仅符合 Unix 系统的设计理念,也使得数据处理程序更加高效、灵活且易于扩展。
2.6打开文件的方式
2.6.1文件打开模式
r(读取:read):打开一个文本文件用于读取。文件必须存在,否则打开失败。打开后,文件指针位于文件的起始位置。
r+(读写):打开一个文本文件用于读取和写入。文件必须存在,否则打开失败。打开后,文件指针位于文件的起始位置。
w(写入:write):打开一个文本文件用于写入。若文件存在,其内容会被清空(即截断到零长度)。若文件不存在,会创建新文件。打开后,文件指针位于文件的起始位置。(跟我们的输出重定向:> 一样)
w+(读写):打开一个文本文件用于读取和写入。若文件存在,其内容会被清空。若文件不存在,会创建新文件。打开后,文件指针位于文件的起始位置。
a(append:追加):打开一个文本文件用于写入。若文件不存在,会创建新文件。写入操作会追加到文件末尾,不会覆盖现有内容。打开后,文件指针位于文件的末尾。(跟我们的追加重定向:>> 一样)
a+(读写):打开一个文本文件用于读取和写入。若文件不存在,会创建新文件。读取时,文件指针位于文件的起始位置;写入时,会追加到文件末尾。打开后,文件指针位于文件的末尾。
注意:将字符串信息写入到文件里,不需要多写\0,因为那是C语言的规定,不是文件的规定!
2.6.2相关文件指针操作函数的复习
fseek:用于移动文件指针到指定位置。常用参数包括文件指针、偏移量和起始位置(如 SEEK_SET
表示文件起始位置)。
ftell:用于获取当前文件指针的位置,即从文件起始位置到当前位置的字节数。返回值是 long int
类型。
rewind:将文件指针重置到文件开头。相当于调用 fseek(fp, 0, SEEK_SET)
,其中 fp
是文件指针。
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *fp;
long int offset;
long int currentPosition;
// 打开文件,假设文件名为"example.txt"
fp = fopen("example.txt", "r+");
if (fp == NULL) {
perror("Error opening file");
return 1;
}
// 移动文件指针到文件开头后100字节的位置
offset = 100;
if (fseek(fp, offset, SEEK_SET) != 0) {
perror("Error seeking in file");
fclose(fp);
return 1;
}
// 获取当前文件指针的位置
currentPosition = ftell(fp);
printf("Current file position: %ld\n", currentPosition);
// 重置文件指针到文件开头
rewind(fp);
printf("File position after rewind: %ld\n", ftell(fp));
// 关闭文件
fclose(fp);
return 0;
}
这些文件打开模式和指针操作函数是 C 语言中文件操作的基础,掌握它们对于进行文件读写操作至关重要。
三.系统文件I/O
打开⽂件的⽅式不仅仅是fopen,ifstream等流式,语⾔层的⽅案,其实系统才是打开⽂件最底层的⽅案。不过,在学习系统⽂件IO之前,先要了解下如何给函数传递标志位,该⽅法在系统⽂件IO接⼝中会使⽤到:
3.1一种传递标志位的方法
就比如系统调用open:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
flags:文件打开模式,用于指定文件的访问方式,正是我们需要给该函数传递标志位!
O_RDONLY
:以只读方式打开文件。O_WRONLY
:以只写方式打开文件。O_RDWR
:以读写方式打开文件。O_CREAT
:如果文件不存在,则创建文件。O_TRUNC
:如果文件已存在且成功打开,则将文件长度截断至 0。O_APPEND
:以追加方式打开文件,写入操作会追加到文件末尾。O_NONBLOCK
:以非阻塞方式打开文件。
mode(可选):用于指定文件的权限模式,仅当 O_CREAT
标志被指定时才有效。它定义了新创建文件的权限。(权限位)
我们之前为我们自己封装的函数要传递我们的标志位,一般是传bool类型,但是bool类型只能传真假,所以有的情况传int,如果有4,5个标志位要同时传,就需要一起传4,5个标志位,可能函数形参的个数就变多了,这个其实挺麻烦的,而实际上,我们传参设置太多,很容易形参实例化,形成多种临时变量,这样是没有必要的,而Linux内核里,未来在进行用户和系统之间调用,传递的标记位,通常是采用位图的方式。
也就是:在 Linux 系统中,open
函数的 flags
参数用于指定文件的打开方式和行为,它通过位图(bitmap)的方式传递多个标志位。位图是一种标记位传参,这些标记位都是一个一个的宏,这些宏只有一个比特位为1。例如,一个整数有32位,用比特位来进行标志位的传递。通过位或(OR)运算符组合使用这些标志位,可以实现复杂的文件操作行为。这种方式可以避免函数形参数量过多,简化函数调用,同时减少因传递多个参数而产生的临时变量,提高代码的可读性和维护性。
我们可以模拟一下比特位传参:
代码演示了如何使用位图(通过位或运算)来传递多个标志位,并在函数中检查这些标志位。
#include <stdio.h>
// 宏定义标志位
#define ONE 0x01 // 0000 0001(1<<0)
#define TWO 0x02 // 0000 0010(1<<1)
#define THREE 0x04 // 0000 0100(1<<2)
#define FOUR 0x08 // 0000 1000(1<<3)
// 函数定义,检查并打印标志位
void func(int flags) {
//并不是 if-else if-else 结构
if (flags & ONE) printf("flags has ONE! ");
if (flags & TWO) printf("flags has TWO! ");
if (flags & THREE) printf("flags has THREE! ");
if (flags & FOUR) printf("flags has FOUR! ");
printf("\n");
}
int main() {
// 调用函数,传递不同的标志位组合
func(ONE); // 仅传递 ONE
func(THREE); // 仅传递 THREE
func(ONE | TWO); // 同时传递 ONE 和 TWO
func(ONE | THREE | TWO); // 同时传递 ONE、TWO 和 THREE
return 0;
}
flags has ONE!
flags has THREE!
flags has ONE! flags has TWO!
flags has ONE! flags has TWO! flags has THREE!
这段代码展示了如何使用位图(通过位或运算)来传递多个标志位,并在函数中检查这些标志位。这种方法在处理多个选项或状态时非常有用,因为它可以减少函数参数的数量,同时保持代码的清晰和易于维护。
3.2myfile.c写文件
我们先来简单的创建文件:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt",O_CREAT|O_WRONLY);
if(fd < 0)
{
perror("open");
return 1;
}
return 0;
}
我们发现:log.txt文件在我们当前路径下创建成功(进程的CWD支持),但是,文件的权限是乱的,是不符合我们创建文件的相关要要求的!
这是为什么?
因为如果我们要新建一个文件的话,那么在采用open系统调用的时候,需要将权限带上:
int fd = open("log.txt",O_CREAT|O_WRONLY,0666);//普通文件的缺省权限:0666
我们rm掉log.txt,执行现在的代码:
我们也可以在我们代码内采用系统调用umask()来设置为全0。(因为就近原则:代码写了,用代码的,没写就用当前底层的0002)
将文件关闭,那怎么关闭呢,这就要用到close:
close
是一个系统调用,用于关闭一个已经打开的文件描述符(file descriptor),释放与该文件描述符关联的资源。当一个程序不再需要访问一个文件时,它应该关闭该文件以释放系统资源,如文件表条目和 I/O 缓冲区。
在 C 语言中,close
函数的原型通常如下所示:
#include <unistd.h>
int close(int fd);
我们就可以使用:close(fd); 进行文件的关闭了。
但是我们还没有写入呢!这时候,我们还需要认识一个接口:
write
是一个系统调用,用于向一个打开的文件描述符(file descriptor)写入数据。它是进行文件输出操作的基本方法之一。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
-
fd:文件描述符,它是一个非负整数,用于标识要写入的文件。
-
buf:指向要写入数据的缓冲区的指针。
-
count:要写入的字节数。
返回写入的字节数。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
int fd = open("log.txt",O_CREAT|O_WRONLY,0666);
if(fd < 0)
{
perror("open");
return 1;
}
//printf("fd: %d\n",fd);
const char * msg = "hello world!\n";
int cnt = 5;
while(cnt--)
{
write(fd,msg,strlen(msg));
}
close(fd);
return 0;
}
lfz@HUAWEI:~/lesson/lesson19/openfile$ cat log.txt
hello world!
hello world!
hello world!
hello world!
hello world!
我们发现代码结果是没有问题的!
但是,我们如果将msg的长度改短一点,并且不带"\n",将cnt该为1,只写一条记录,我们执行完成之后,cat写入文件,我们发现:
这怎么跟我们上面的"w",写的时候,他是清空写入呀,为什么这里只是单纯的从开头覆盖写呢?
原因是我们在open打开文件的时候,只告诉OS的open()说,我要创建(O_CREAT)我要写入(O_WRONLY),并没有要求要清空!!!我们还需要加上(O_TRUNC):这也意味着写入的逻辑注释之后,运行就会使目标文件内容清空!!!
我们就可以得到一个较为合适的代码了:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
int fd = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
if(fd < 0)
{
perror("open");
return 1;
}
//printf("fd: %d\n",fd);
const char * msg = "abcd";
int cnt = 1;
while(cnt--)
{
write(fd,msg,strlen(msg));
}
close(fd);
return 0;
}
我们还有追加这一概念,追加也是写入,我们知道O_TRUNC是清空,然后从文件开头写,所以这肯定是与追加相矛盾的,我们应该换成追加(O_APPEND)
如果我们比较仔细的话,我们可以看到write的buf参数不是const char*,而是const void*,说明可以用于二进制写入,也可以做字符串写入,说到这里,我们来谈谈:
文本写入 VS 二进制写入
我们知道,在显示器(文件)上打印出"12345",打印的是一个个的字符,不是打印数字的12345, 我们来试一下:
我们发现目标文件打印出来的是乱码,其实是因为,我们在写入时,是将a这个整型变量写到了目标文件里,所以我们可以发现文件的大小是4个字节:
整数是4个字节,而写入的1234567是不可显的!!!他就是将这个1234567这个数字将其二进制写到对应的磁盘上了,但是我们想要看到的是1234567呀,所以我们应该当作字符串来写:
//当作字符串来写--->
char buffer[16];
snprintf(buffer,sizeof(buffer),"%d",a);//将其格式化成字符串
write(fd,buffer,strlen(buffer));
现象出来了,是该出结论了:
在系统层面上不存在所谓的文本写入,或者二进制写入,也就是系统是不关心我们用户的写入类型的!文本写入和二进制写入时语言层的概念:
3.3myfile.c读文件
能够写入文件了,那我们这时候也该认识如何去读文件了:
read
是一个系统调用,用于从文件描述符中读取数据。以下是关于read
系统调用的详细信息:
read
系统调用用于从文件描述符(fd
)中读取数据,并将其存储到指定的缓冲区(buf
)中。它支持从文件、设备、套接字、管道等多种对象中读取数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
-
fd
:文件描述符,表示要从中读取数据的文件或设备。 -
buf
:指向缓冲区的指针,用于存储读取的数据。 -
count
:指定要读取的最大字节数。 -
成功时,返回实际读取的字节数。如果返回值为0,表示已经到达文件末尾。失败返回 -1.
简单来讲:fd的文件内容读到buf当中。
我们读文件就需要代开文件,而且读文件的话,这个目标文件就不应该被新建了,我们所读的文件是一定存在的,我们open目标文件就不需要O_CREAT,而且要以读(O_RDONLY)方式打开,当然也是不需要权限参数的,这也是为什么open会提供两个函数接口。
我们的myfile.c的读文件代码就出来了:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
//int fd = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
//int fd = open("log.txt",O_CREAT|O_WRONLY|O_APPEND,0666);
int fd = open("log.txt",O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
while(1)
{
char buffer[64];
int n = read(fd,buffer,sizeof(buffer)-1);//-1是为了给字符串至少留一个停止标志
if(n > 0)
{
buffer[n] = 0;
printf("%s",buffer);
}
else if(n == 0)//是不是和我们的feof有点......嘻嘻
{
break;
}
}
close(fd);
return 0;
}
3.4文件描述符
一直说文件描述符,那我们fd到底是什么东西呢?他是open()的返回值,那我们来打印看看他打开文件成功的返回值是什么:
fd: 3
3到底是什么意思呢? 我们竟然能够根据一个整数进行文件的读写操作,我们之前写的FILE*文件指针又是什么鬼?
fd的值是3,也就是说进程拿着数字3就可以进行文件读写了,那我要是多建几个文件呢?
很明显,我们文件open成功的返回值是>0的,我们打印出来的是3·4·5·6,那么0·1·2去哪里了呢?
3.4.1 0 & 1 & 2
记不记得我们上面的标准输入·输出·错误,对应的文件描述符就是0·1·2,也就是默认打开的三个文件,0·1·2已经被占用了!!!
那我们之前所遇到的FILE是什么呢?又跟fd有什么关系呢?
其实FILE就是一个C语言提供的一个结构体,在语言层其实就是:
typedef XXX
{
//属性....
}FILE;
FILE是被typedef出来的!!!
注意:在操作系统接口层面上,只认fd,即文件描述符!!!
这些库函数底层是一定封装了系统调用接口,所以我们可以大胆预测FILE结构体当中,一定封装了文件描述符!!!
我们可以来简单谈谈语言封装:
对于C程序启动,会默认打开三个标准文件,那么C++呢,Java?Python?PHP?Go?......
C++要读取文件是采用IO流(iostream),其实就连C++采用的文件操作都和C语言不一样,所有语言也就跟别说了,基本都不是一套,都有属于自己的文件相关操作。C++默认打开的:cin/cout/cerr,这三个是类的对象,里面也是一定封装了文件描述符,不管是什么语言的封装,底层就一套方法---系统调用!!!
总结来说,这张图强调了在编写跨平台代码时,需要考虑不同操作系统的底层接口差异,并通过条件编译等技术手段来实现代码的可移植性。这样,开发者可以在语言层面(如C/C++)编写代码,而底层的兼容性问题则通过适当的封装和裁剪来解决。
也就是说,操作系统的系统调用接口都是不一样的,我们要研究可移植性,首先要先想清楚,什么叫做不可移植性:
不可移植性(Non-portability)指的是软件、代码或程序在从一个计算环境(如操作系统、硬件平台或设备)转移到另一个环境时,不能正常工作或需要进行大量修改的特性。主要还是平台带来的差异!
平台不一样,也就是操作系统不一样,系统调用接口不一样,所以移植性就差,所以为了支持语言的可移植性,就好比说:C/C++,他就把所有平台的文件操作全都实现了一份,等于说10个平台,那么他就有10份一样的代码,大家的底层接口不一样,但是,对他来说,在底层不一样,就在底层封装,因为用户不使用系统调用,这也就是为什么在Linux,要安装Linux的库,在Windows里,要安装Windows的相关库。
所以语言做封装就可以增加了自己的可移植性,但是语言为什么要增加自己的可移植性呢?
提高可移植性可以为编程语言带来很多好处,包括扩大用户基础、提高开发效率、促进代码重用、增强竞争力等。这也是为什么很多编程语言都非常重视可移植性,并在设计和实现中采取各种措施来提高自身的可移植性。
好啦,上层的我们说完了,我们来说说下层:
3.4.2什么是文件描述符
Linux内核中进程打开文件的管理机制主要通过task_struct
、files_struct
和file
结构体来实现。
以下是详细的机制描述:
1. task_struct
结构体
task_struct
是Linux内核中描述进程的主要数据结构,它包含了进程的所有信息,包括进程状态、进程ID、文件描述符表等。每个进程都有一个唯一的task_struct
实例,内核通过这个结构体来管理进程。
2. files_struct
结构体
每个进程都有一个files_struct
结构体,它用于管理进程打开的文件。files_struct
包含以下关键部分:
-
文件描述符表(fdtable):
files_struct
通过一个指针指向fdtable
结构体,该结构体包含文件描述符数组和其他相关信息。
fdtable
结构体:这是文件描述符表的主要数据结构,它包含了一个指向file
结构体数组的指针(即fd_array
),以及一些其他信息,如文件描述符表的大小等。
fd_array
:这是fdtable
结构体中的一个字段,它是一个数组,每个元素都是一个指向file
结构体的指针。这个数组用于存储进程打开的所有文件的引用。
所以文件描述符的本质就是数组的下标!!!
-
文件描述符数组:文件描述符数组是一个指针数组,每个元素指向一个
file
结构体,表示一个打开的文件。
3. file
结构体
file
结构体表示一个已经打开的文件对象,它保存了文件相关的信息,如文件状态、文件操作等。每个打开的文件在内核中都有一个file
对象。
4. 文件描述符的管理
-
文件描述符数组的扩展:当进程打开的文件数量超过当前文件描述符数组的大小时,
fdtable
会被扩展,创建一个新的更大的fdtable
结构体。 -
锁机制:在新的无锁(lock-free)模型中,文件描述符表的更新基于读-复制-更新(RCU)机制,以确保更新操作对读取操作是原子的。
-
文件查找:查找文件结构体时,需要使用
lookup_fdget_rcu()
或files_lookup_fdget_rcu()
等API,这些API会处理无锁查找中的内存屏障问题。
5. 文件的打开与关闭
-
打开文件:当进程调用
open
系统调用时,内核会创建一个新的file
对象,并将其添加到进程的文件描述符数组中。 -
关闭文件:当进程调用
close
系统调用时,内核会从文件描述符数组中移除对应的file
对象,并释放相关资源。
6. 文件描述符的引用计数
-
引用计数:
file
结构体使用引用计数(f_count
)来管理其生命周期。当文件被打开时,引用计数增加;当文件被关闭时,引用计数减少。当引用计数为0时,内核会释放file
对象。
7. 文件描述符的共享
-
共享文件描述符表:在某些情况下(如使用
CLONE_FILES
标志创建的线程),多个进程可以共享同一个files_struct
结构体。
Linux内核通过task_struct
、files_struct
和file
结构体来管理进程打开的文件。文件描述符是文件描述符数组的索引,通过文件描述符可以找到对应的file
对象。内核使用锁机制和RCU来确保文件描述符表的更新操作是安全的。
文献参考:
: File management in the Linux kernel
: File management in the Linux kernel
: Linux Kernel Process Management | Process Descriptor and …
在Linux内核中,读写文件的过程涉及到文件描述符、文件结构体(file
)、文件描述符表(fdtable
)以及页缓存(page cache)等概念。当进程需要读取文件时,它首先通过文件描述符找到对应的file
结构体。文件描述符是进程通过open
系统调用获得的,并且被存储在进程的files_struct
中的fd_array
数组里。fd_array
是一个指针数组,用于存储指向file
结构体的指针。每个指针都指向一个打开的文件的file
结构体,而文件描述符就是该指针数组的下标。内核使用文件描述符作为索引,在fd_array
中找到相应的file
结构体,然后通过该结构体中的信息(如文件位置、文件状态等)来定位文件数据,并将其从磁盘读入进程的地址空间。在这个过程中,数据会先被拷贝到操作系统内核的缓冲区中,也就是页缓存(page cache),然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。这种缓存机制可以减少读盘的次数,从而提高性能。
写入文件的过程与读取类似。进程使用write
系统调用,并将文件描述符作为参数传递给内核。内核通过文件描述符在fd_array
中定位到正确的file
结构体,然后根据该结构体中的信息来定位文件数据的位置,并将进程提供的数据写入到磁盘上。写入操作可能会触发内核的缓存机制,数据首先被写入到文件系统的缓存中,然后再异步地写回到磁盘上,以提高写入效率。在整个过程中,file
结构体确保了文件操作的原子性和一致性。
文件修改操作也是可以高效地执行,因为数据首先加载到在内存,在内存中缓存区上进行修改,然后再异步地写回到磁盘上。这减少了磁盘I/O操作的次数,提高了文件系统的性能。同时,页缓存也提供了数据的一致性和原子性,确保了文件操作的正确性。
总的来说:对文件内容做任何操作,都必须先把文件加载(磁盘到内存的拷贝)到内核对应的文件缓存区内!!!
3.4.3文件描述符分配原则
我们C程序默认打开三个文件,我们可以close(0):
close(2):
3.4.4重定向原理
我们close(1)呢?
注意,我们下面这个代码是打开没有关闭的,即没有close(fd);
根据文件描述符的分配规则,fd的之应该是1,我们" ll "指令发现多了一个文件log.txt:
我们cat发现,本来应该写入到显示器文件的内容,竟然写到了log.txt文件里!!!
这个现象就叫做重定向!!!
printf其实就是往stdout文件打印的,对应stdout封装的文件描述符就是1,但是我们的前两行代码是对OS做的,所以他只认识文件描述符,就导致文件内容打印到了log.txt文件了!!!
所以重定向的本质就是在内核当中做的“狸猫换太子”!!!就是将标准输出,改成了从指定文件写入,这就叫做输出重定向!!!
重定向的本质就是在操作系统内核中对文件描述符进行重新映射,它并不改变用户程序的代码逻辑,而是改变了程序执行时文件描述符所指向的目标。在Linux内核中,每个进程都有一个文件描述符数组,其中标准输出文件描述符(通常为1)默认指向终端。当进行输出重定向时,内核将标准输出的文件描述符从终端改为指向一个指定的文件,这样原本输出到终端的数据就会被写入到该文件中。从上层应用的角度来看,文件描述符数组的下标(如1)没有变化,但是这个下标所指向的file
结构体(代表打开的文件)已经改变,从而改变了数据的流向。这种机制使得程序无需修改即可改变输出目标,增强了程序的灵活性和可移植性。
下面,我们来演示一种现象,这种现象和曾经我们学习过的一个知识是关联的,我们先来见见:
我们发现我们新增一行关闭文件的代码,log.txt文件内容就变为空的了!?我们先将现象放在这,在缓冲区专题谈。
现在我们是知道重定向的本质就是更改作为数组下标的文件描述符的指向的内容!但是我们上面这种狸猫换太子的做法太挫了,我们先来认识一个接口:
dup2
是一个 POSIX 标准的函数,用于在 Unix 和类 Unix 操作系统中复制文件描述符。它的作用是将一个已经打开的文件的文件描述符复制到另一个文件描述符上,同时关闭原先的文件描述符(如果需要的话)。(用于重定向的系统调用)
int dup2(int oldfd, int newfd);
-
oldfd
:这是已经打开的文件的文件描述符,你想要从这个文件描述符复制。 -
newfd
:这是目标文件描述符。dup2
会将oldfd
指向的文件与newfd
关联起来。如果newfd
已经打开,它会被关闭,然后被oldfd
指向的文件替换。 -
成功时返回新的文件描述符(通常与
newfd
相同)。
我们需要清楚我们传参数的顺序,是1,fd还是fd,1?
我们假设fd就是3,我们想让1指向3(fd),我们要让3指向的内容覆盖到1指向的内容,将3里面的地址覆盖到1里面,1就不在指向标准输入了,所以我们应该是:
dup2(fd, 1);
就是交换一下,喝汤该轮到我了!:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//close(1)
int fd = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
if(fd < 0) exit(1);
dup2(fd, 1);
close(fd);
printf("fd: %d\n", fd);
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
fprintf(stdout,"hello hello\n");
fprintf(stdout,"hello hello\n");
fprintf(stdout,"hello hello\n");
fprintf(stdout,"hello hello\n");
//close(1);//close(fd)//使用这行代码,会出现一个奇怪现象???
return 0;
}
这段代码演示了如何使用 open
和 dup2
函数来重定向标准输出(通常是文件描述符 1)到一个文件。
如果在程序中使用dup2
函数复制文件描述符,并且不关闭原始的文件描述符(即不调用close(fd)
),通常不会有问题;
尽管如此,最佳实践仍然是在不再需要文件描述符时关闭它们,以避免潜在的问题。这不仅可以释放资源,还可以避免文件描述符泄漏,特别是在长时间运行的程序中。
在某些情况下,你可能需要保留原始文件描述符,例如,当你需要在程序的不同部分使用不同的文件描述符来访问同一个文件时。在这种情况下,不关闭原始文件描述符是合理的。
总之,虽然在某些情况下不关闭文件描述符可能不会立即导致问题,但为了编写健壮、可维护的代码,建议在不再需要文件描述符时关闭它们。
对于我们的追加重定向,我们只需要将open()的标识符O_TRUNC改成O_APPEND就行了!!!
int fd = open("log.txt",O_CREAT|O_WRONLY|O_APPEND,0666);
现在我们文件里有内容了,我们就可以使用标准输入,就是0下标的汤要换人喝了:(输入重定向)
int main()
{
int fd = open("log.txt",O_RDONLY,0666);
if(fd < 0) exit(1);
dup2(fd, 0);
close(fd);
while(1)
{
char buffer[64];
if(!fgets(buffer,sizeof(buffer),stdin))break;
printf("%s",buffer);
}
return 0;
}
本来是该在标准输入读的,现在在log.txt文件当中读:
总的来说,我们重定向的本质就是打开文件的方式+dup2!!!
这时候,我们也就可以结合命令行参数,就可以实现指定文件内容的读取了:
int main(int argc, char *argv[])
{
if(argc != 2) exit(1);
int fd = open(argv[1],O_RDONLY,0666);
if(fd < 0) exit(1);
dup2(fd, 0);
close(fd);
while(1)
{
char buffer[64];
if(!fgets(buffer,sizeof(buffer),stdin))break;
printf("%s",buffer);
}
return 0;
}
3.4.5自主Shell新增功能
我们就可以在我们的自主Shell当中实现重定向的功能了:
我们要注意,我们不可以让父进程重定向,因为父进程一重定向会影响当前父bash的文件打开情况,重定向一定是让子进程进行的,每个进程都有自己的PCB,都有自己的文件描述符表,所以让子进程重定向就不会影响父进程。
还有需要知道,进程替换会不会影响重定向的结果吗?我今天进行重定向了,会不会影响我这个进程曾经打开的文件?
当我们一个进程在进行重定向的时候不会影响曾经打开的文件,毫不影响的,因为进程有自己的文件描述符,有自己的页表,虚拟地址空间等,进程替换的本质是代码和数据的替换,并没有创建新进程,整体内核这套模块没有变换,打开文件属于文件那一套,重定向也是要打开文件,所以进程替换不影响重定向的结果.
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#include <unordered_map> // hash
#define COMMAND_SIZE 1024
// 命令行格式
#define FORMAT "[%s@%s %s]# "
// 下面是Shell定义的全局数据
#define MAXARGC 128
#define MAXENVP 100
// 命令行参数/参数表
char *g_argv[MAXARGC];
int g_argc = 0;
// 环境变量表
char *g_env[MAXENVP];
int g_envs = 0; // 环境变量的个数
// 别名映射表
std::unordered_map<std::string, std::string> alias_list;
// last exit code
int lastcode = 0;
// for test
char cwd[1024];
char cwdenv[1024];
//关于重定向,我们关系的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
// #define TrimSpace(CMD, index) do {
// while (isspace(*(CMD+index)))
// {
// index++;
// }
// } while(0)
int redir = NONE_REDIR;
std::string filename;
// last_cwd 用于记录上一次的工作目录
char last_cwd[1024] = {0};
// 获取用户名
const char *GetUserName() {
const char *name = getenv("USER");
return name == NULL ? "None" : name;
}
// 获取主机名
const char *GetHostName() {
const char *hostname = getenv("HOSTNAME");
return hostname == NULL ? "None" : hostname;
}
// 获取当前路径
const char *GetPwd() {
const char *pwd = getcwd(cwd, sizeof(cwd));
if (pwd != nullptr) {
snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", pwd);
putenv(cwdenv);
}
return pwd == NULL ? "None" : pwd;
}
// 获取家目录
const char *GetHome() {
const char *home = getenv("HOME");
return home == NULL ? "None" : home;
}
// 获取环境变量表
void InitEnv() {
extern char **environ;
memset(g_env, 0, sizeof(g_env));
// 从父Shell获取环境变量
for (int i = 0; environ[i]; i++) {
g_env[i] = strdup(environ[i]); // 使用 strdup 复制字符串
g_envs++;
}
g_env[g_envs++] = strdup("HAHA=for_test"); // 添加一个测试环境变量
g_env[g_envs] = NULL;
// 将环境变量表赋给 environ
environ = g_env;
}
// cd 命令
bool Cd() {
if (g_argc == 1) {
// 如果没有指定路径,默认切换到家目录
std::string home = GetHome();
if (home.empty()) {
std::cerr << "cd: HOME not set" << std::endl;
return false;
}
chdir(home.c_str());
} else {
std::string where = g_argv[1];
if (where == "~") {
// 切换到家目录
std::string home = GetHome();
if (home.empty()) {
std::cerr << "cd: HOME not set" << std::endl;
return false;
}
chdir(home.c_str());
} else if (where == "-") {
// 切换到上一次的工作目录
if (last_cwd[0] != '\0') {
chdir(last_cwd);
} else {
std::cerr << "cd: no previous directory saved" << std::endl;
return false;
}
} else {
// 切换到指定目录
if (chdir(where.c_str()) != 0) {
std::cerr << "cd: " << where << ": No such file or directory" << std::endl;
return false;
}
}
}
// 更新 last_cwd 为当前目录
getcwd(last_cwd, sizeof(last_cwd));
return true;
}
// echo 命令
bool Echo() {
if (g_argc == 2) {
std::string opt = g_argv[1];
if (opt == "$?") {
std::cout << lastcode << std::endl;
lastcode = 0; // 清0
} else if (opt[0] == '$') {
std::string env_name = opt.substr(1);
const char *env_value = getenv(env_name.c_str());
if (env_value) {
std::cout << env_value << std::endl;
} else {
std::cout << std::endl;
}
} else {
std::cout << opt << std::endl;
}
}
return true;
}
// export 命令
bool Export() {
if (g_argc < 2) {
std::cerr << "export: not enough arguments" << std::endl;
return true;
}
for (int i = 1; i < g_argc; ++i) {
std::string env_str = g_argv[i];
size_t equal_pos = env_str.find('=');
if (equal_pos == std::string::npos) {
std::cerr << "export: invalid variable name" << std::endl;
return true;
}
std::string key = env_str.substr(0, equal_pos);
std::string value = env_str.substr(equal_pos + 1);
char *env_entry = new char[key.size() + value.size() + 2];
sprintf(env_entry, "%s=%s", key.c_str(), value.c_str());
putenv(env_entry);
g_env[g_envs++] = env_entry;
}
return true;
}
// alias 命令
bool Alias() {
if (g_argc == 1) {
// 显示所有别名
for (const auto &entry : alias_list) {
std::cout << entry.first << "=" << entry.second << std::endl;
}
} else if (g_argc == 2) {
// 删除别名
std::string nickname = g_argv[1];
if (alias_list.find(nickname) != alias_list.end()) {
alias_list.erase(nickname);
} else {
std::cerr << "alias: " << nickname << ": not found" << std::endl;
}
} else if (g_argc == 3) {
// 添加别名
std::string nickname = g_argv[1];
std::string target = g_argv[2];
alias_list[nickname] = target;
} else {
std::cerr << "alias: invalid arguments" << std::endl;
}
return true;
}
// 获取当前路径的父目录
std::string DirName(const char *pwd) {
std::string dir = pwd;
if (dir == "/") return "/";
auto pos = dir.rfind('/');
if (pos == std::string::npos) return "/";
return dir.substr(0, pos);
}
// 初始化命令行
bool MakeCommandLine(char cmd_prompt[], int size) {
snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
return true;
}
// 打印命令行提示符
void PrintCommandPrompt() {
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt, sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}
// 获取用户输入的命令
bool GetCommandLine(char *out, int size) {
char *c = fgets(out, size, stdin);
if (c == NULL) {
return false;
}
out[strcspn(out, "\n")] = 0; // 去掉换行符
if (strlen(out) == 0) {
return false;
}
return true;
}
// 命令行分析
bool CommandParse(char *commandline) {
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, " ");
while ((g_argv[g_argc++] = strtok(NULL, " ")));
g_argc--;
// 检查别名
std::string cmd = g_argv[0];
auto it = alias_list.find(cmd);
if (it != alias_list.end()) {
// 如果是别名,替换为实际命令
std::string new_cmd = it->second;
delete[] g_argv[0]; // 释放旧的命令
g_argv[0] = new char[new_cmd.size() + 1];
strcpy(g_argv[0], new_cmd.c_str());
}
return g_argc > 0;
}
// 打印解析后的命令和参数
void PrintArgv() {
for (int i = 0; i < g_argc; ++i) {
printf("g_argv[%d]->%s\n", i, g_argv[i]);
}
}
// 检测并处理内建命令
bool CheckAndExecBuiltin() {
std::string cmd = g_argv[0];
if (cmd == "cd") {
return Cd();
} else if (cmd == "echo") {
return Echo();
} else if (cmd == "export") {
return Export();
} else if (cmd == "alias") {
return Alias();
}
return false;
}
// 执行命令
int Execute() {
pid_t id = fork();
if (id == 0) {
int fd = -1;
//子进程检查重定向情况
if(redir == INPUT_REDIR)
{
fd = open(filename.c_str(), O_RDONLY);
if(fd < 0) exit(1);
dup2(fd,0);
close(fd);
}
else if(redir == OUTPUT_REDIR)
{
fd = open(filename.c_str(), O_CREAT|O_WRONLY|O_TRUNC,0666);
if(fd < 0) exit(2);
dup2(fd,1);
close(fd);
}
else if(redir == APPEND_REDIR)
{
fd = open(filename.c_str(), O_CREAT|O_WRONLY|O_APPEND,0666);
if(fd < 0) exit(3);
dup2(fd,1);
close(fd);
}
else
{
//没有重定向 DO Nothing
}
//child
execvp(g_argv[0], g_argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) {
lastcode = WEXITSTATUS(status);
}
return 0;
}
//去空格的接口
void TrimSpace(char cmd[], int& end)
{
while(isspace(cmd[end]))
{
end++;
}
}
//重定向检查
void RedirCheck(char cmd[])
{
//清空
redir = NONE_REDIR;
filename.clear();
int start = 0;
int end = strlen(cmd)-1;
//"ls -a -l > file.txt"//> >> <
//逆着来
while(end > start)
{
if(cmd[end] == '<')
{
cmd[end++] = 0;
TrimSpace(cmd,end);//end下标指向f
redir = INPUT_REDIR;
filename = cmd+end;
break;
}
else if(cmd[end] == '>')
{
if(cmd[end-1] == '>')
{
cmd[end-1] = 0;
redir = APPEND_REDIR;
}
else
{
redir = OUTPUT_REDIR;
}
cmd[end++] = 0;
TrimSpace(cmd,end);//end下标指向f
filename = cmd + end;
break;
}
else
{
end--;
}
}
}
// 清理函数
void Clear() {
for (int i = 0; i < g_envs; ++i) {
free(g_env[i]);
}
}
int main()
{
//Shell启动的时候,从系统中获取环境变量
InitEnv();
while (1) //命令行不会停止,要不断获得用户输入
{
//1.输出命令行提示符
PrintCommandPrompt();
//2.获取用户输入的命令
char commandline[COMMAND_SIZE];
if (!GetCommandLine(commandline, COMMAND_SIZE))
{
continue; //输入有问题就重新输入
}
//additional_3.重定向分析"ls -a -l file.txt" -> "ls -a -l" "file.txt" -> 判定重定向方式
//左半部分才是下面3要分析的
RedirCheck(commandline);
//测试
//printf("redir :%d, filename: %s\n", redir, filename.c_str());
//3.命令行分析"ls -a -l"->"ls" "-a" "-l",未来要使用程序替换,这种形式的参数,方便调用!!!
if(!CommandParse(commandline))
{
continue;//如果解析失败,不执行以下代码了,解析成功才可执行!!!
}
//sub_4.检测并处理内建命令
if(CheckAndExecBuiltin())
{
continue;
}
//4.执行命令
Execute();
}
//清理函数
Clear();
return 0;
}
我们发现这些外置命令的重定向效果都是可以的,我们内建命令也是可以进行重定向的,比如说echo,但是有一个痛点:
如果内建命令做重定向,就会更改Shell的标准输入·输出·错误。本来我的Shell就应该从键盘里读数据,现在给我改了?!那么,这里,我们该如何理解呢?
在Linux内核中,内建命令的重定向是通过文件描述符和页缓存机制实现的。当在Shell中执行重定向操作时,Shell程序内部会调用系统调用(如dup2
),将标准输入、输出或错误流的文件描述符指向新的文件。内核利用页缓存(page cache)临时保存从文件系统读取的数据或等待写入磁盘的数据,这样,即使在数据实际写入磁盘之前,Shell也可以继续执行其他命令,因为数据已经暂存在内存中。这种机制允许Shell高效地管理文件描述符和数据流,实现内建命令的重定向功能,而无需等待磁盘I/O操作完成,从而提高整体的系统性能和响应速度。
下面简单展示如何实现内建命令echo
的重定向:(功能比较不全,缺少的功能可自行补充)
// echo 命令
bool Echo() {
if (g_argc > 1) {
std::string output;
for (int i = 1; i < g_argc; ++i) {
output += g_argv[i];
output += " ";
}
output.pop_back(); // 去掉最后一个空格
if (redir == OUTPUT_REDIR || redir == APPEND_REDIR) {
int fd = open(filename.c_str(), redir == OUTPUT_REDIR ? O_CREAT | O_WRONLY | O_TRUNC : O_CREAT | O_WRONLY | O_APPEND, 0666);
if (fd < 0) {
std::cerr << "echo: failed to open file" << std::endl;
return false;
}
write(fd, output.c_str(), output.size());
close(fd);
} else {
std::cout << output << std::endl;
}
}
return true;
}
我们还有一个问题:
我们原本3指向的内容覆盖了1指向的内容,这时候1和3都指向该文件,那么1曾经打开过的文件:标准输出该怎么办呀,这是怎么处理了?
一个文件是可以被多个进程打开的!(我们之前fork后,父子都可以往显示器文件打,这是很明显的例子)所以一个文件被多个进程打开,那么自己进程所对应得PCB内都有files_struct*指向files_struct对象,该对象都有指向file。当使用dup2
函数进行文件描述符的复制和重定向时,比如将文件描述符3的内容覆盖文件描述符1(标准输出)指向的内容,此时文件描述符1和3都指向同一个文件。对于文件描述符1曾经打开的文件,即标准输出(通常是终端),内核会通过引用计数机制来管理。引用计数记录了文件被多少个文件描述符引用,当引用计数为0时,内核会释放该文件的资源。
在Linux内核中,每个file
结构体都有一个引用计数,表示有多少个文件描述符指向该文件。当通过dup2
进行重定向时,实际上是将新的文件描述符指向已存在的file
结构体,从而增加了引用计数。当不再需要某个文件描述符时,比如通过close
函数关闭它,引用计数会相应减少。如果引用计数变为零,则表示没有文件描述符引用该文件,内核可以释放与该文件相关的资源,包括页缓存中的数据。这样,即使文件被多个进程或多个文件描述符打开,内核也能有效地管理文件的生命周期和资源释放。