当前位置: 首页 > news >正文

Linux的基础IO

目录

先来段代码回顾C文件接口

系统文件I/O

为什么是调用接口?而不是函数?

open接口介绍

从操作系统视角来看文件管理 

文件对流的管理--文件描述符fd

文件描述符的分配规则

write接口介绍

 read接口介绍

 重定向

重定向的原理

 dup2系统调用

添加重定向功能到minishell 

为什么重定向后不会受影响?


先来段代码回顾C文件接口

test.c向文件写入

#include <stdio.h>
#include <string.h>
#include <stdlib.h>  int main()
{FILE* pf;pf = fopen("test.txt", "w");if (!pf){printf("fopen fail\n");exit(1);}const char* msg = "hello world\n";int cnt = 5;while (cnt--){fwrite(msg, strlen(msg), 1, pf);}fclose(pf);return 0;
}

文件内容:

test.c向文件中读取

#include <stdio.h>
#include <string.h>
#include <stdlib.h>int main()
{FILE *pf = fopen("test.txt", "r");if(!pf){printf("fopen fail\n");}char buf[1024];const char* msg = "hello world\n";while(1){//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明ssize_t s = fread(buf, 1, strlen(msg), pf);if(s > 0){buf[s] = 0;printf("%s",buf);}if(feof(pf)){break;}}fclose(pf);return 0;
}

在Xshell上运行结果:

我们引用一段小小的代码简单回忆C语言的文件操作。那么下面就为大家推荐几篇文章,然后更好的复习下。

c语言的文件操作与文件缓冲区-CSDN博客

字符串格式化函数sprintf和snprintf的详解-CSDN博客

字符串处理函数:sscanf 的用法-CSDN博客

那么下面就转接到Linux的角度来解释一下IO了。

系统文件I/O

如果对C语言中的文件操作是说缓冲文件系统,那么下面的就是非缓冲文件系统,也就是系统的文件I/O函数比较偏底层一点。当然这是系统提供的一些函数,但是C语言本身也是有接口概念的,只不过接口的形式和高级编程语言(如Java、C++中的interface)有所不同。但是大同小异,实际上都是调用的接口。

这时候就会有疑惑了?为什么是要调用接口?我直接调用函数不行么?就像我自己实现一个add函数,然后自己调用,这样解释起来更好让我理解啊,并且还十分方便记忆。

为什么是调用接口?而不是函数?

那么,这里我们温习一下操作系统的概念

我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。

这就是因为在根本上操作系统确实像银行一样,并不完全信任用户程序,因为直接开放底层资源(如内存、磁盘、硬件访问权限)给用户程序会带来巨大的风险。所以就向银行一样他的服务是由工作人员隔着一层玻璃,然后对顾客进行服务的。所以我们在调用一些库已经给的函数的时候,并不能说我们调用了函数,而是要说调用了由系统封装过后的接口。因此,操作系统设计了一套系统调用接口,就像银行柜台的玻璃一样,用户程序只能通过这些“窗口”来请求资源和服务。即使最常用的printf函数也是如此。

这也就实现了对于C语言它实际只是定义了一套的库函数,然后Windows与Linux通过不同的封装方法,从而可以实现C语言的跨平台性。

总结:用户程序 → C标准库函数(封装后的接口)→ 系统调用接口 → 操作系统内核



我们就可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码:

hello.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);// 设置umask为0,这样在此进程下创建的文件所有权限都是默认全给的int fd = open("myfile", O_WRONLY|O_CREAT, 0644);if(fd < 0){perror("open");return 1;}int count = 5;const char *msg="hello world\n";int len=strlen(msg);while(count--){write(fd, msg, len);;//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数//据。 返回值:实际写了多少字节数据}close(fd);return 0;
}

在Xshell下运行结果: 

hello.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);// 设置umask为0,这样在此进程下创建的文件所有权限都是默认全给的int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}char buf[1024];const char *msg="hello world\n";int len=strlen(msg);while(1){ssize_t s = read(fd, buf, strlen(msg));//类比writeif(s > 0){printf("%s",buf);}else{break;}}close(fd);return 0;
}

在Xshell下运行结果:

open接口介绍

 这里我们一步一步来,我们先介绍接口,然后再补充操作系统的知识。

对于接口的认识,是建议直接去man然后去阅读原文文档。

指令:

man open

下面就由我来介绍一下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);pathname: 要打开或创建的目标文件
flags : 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数 :O_RDONLY: 只读打开O_WRONLY : 只写打开O_RDWR : 读,写打开这三个常量,必须指定一个且只能指定一个O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限O_APPEND : 追加写
返回值:成功:新打开的文件描述符失败: -1

注意点:

第一个参数

  • 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
  • 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)。

第二个参数

  • 打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用“或”运算符隔开。
  • 例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:
    O_WRONLY | O_CREAT

第三个参数 

这个参数比较特殊只是在目标文件不存在,则需要open进行创建,第三个参数,就是表示创建文件的默认权限。反之则只使用前两个参数。

这里就不多介绍了,也不做解释了,由不明白的可以去看下面这篇文章对文件掩码复习一下。

Xshell,Shell的相关介绍与Linux中的权限问题_xshell sudo获取权限-CSDN博客

  • 若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。 
umask(0); //将文件默认掩码设置为0

对于返回值的介绍我放在下面 的从操作系统视角来看文件管理 部分。

从操作系统视角来看文件管理 

那么下面我们就转到操作系统的脚步来看用户打开一个文件,操作系统又做了什么。

就比如下面的一段代码:

#include <stdio.h>
#include <fcntl.h>
#include <string.h>int main()
{int fd = open("test.txt", O_RDONLY);// 文件操作代码...close(fd);return 0;
}

从用户的角度来看,那他就简简单单的调用了open接口,然后就进行一些文件操作,然后再关闭文件,但是在操作系统的角度来看,其实并没有那么简单,其实是做了一系列复杂的操作。

首先,当用户调用open接口的时候,操作系统首先会为文件分配一个 文件描述符(或文件流)。每个打开的文件在操作系统中都对应一个文件描述符(文件描述符表)。

  • 文件描述符 是一个非负整数,它用于标识和管理打开的文件。
  • 操作系统会维护一个文件描述符表,用于跟踪每个文件描述符所对应的文件资源(例如,文件的读写指针、文件类型、文件系统上的实际位置等)。

当用户调用open接口的时候,操作系统从 文件描述符表 中分配一个空闲的描述符。如果该 文件描述符表 已经被占用,那么操作系统就会下一个可用的文件描述符开始分配。如果有感觉的话,其实这个过程很像遍历一个数组,从下标0开始遍历,直到找到一个空的空间然后分配数据。(其实这个管理 文件描述符表 的确是是一个数组。这个数组就叫文件表)

在完成上述的步骤后,文件还没有完全的打开,那么下一步就是打开文件,并更新刚才的数据。

上一步操作没问的话,现在的 文件描述符表 已经找到了空间,那么接下来操作系统就会进行下面的步骤:

  • 内核空间 中打开文件。操作系统会通过文件路径定位文件,并检查文件是否存在。如果文件不存在,并且用户指定了 O_CREAT 标志,操作系统会创建文件。
  • 更新文件系统的文件表。操作系统会在内部维护一个文件表(文件系统表)来跟踪文件的状态。这包括文件的读写位置、访问模式、权限等信息。

 到这里才真正的打开了文件。并且还可以通过文件表的下标进行维护。(操作系统还会做一些一些别的操作,但到这里已经打开了文件,下面的一些操作我们这里并不关心,就不说了)

那么我们就用刚才的知识就可以总结出来

操作系统对于维护文件是通过文件表数组来维护的,每一个元素都是一个结构体。结构体内存储的各种文件的信息。每一个打开的文件都对应一个文件表的下标,从0开始标记。

那么对上面的代码修改,打印看看下表。

注意:open函数的返回值就为该文件所在文件表的下标。

#include <stdio.h>
#include <fcntl.h>
#include <string.h>int main()
{int fd = open("test.txt", O_RDONLY);// 文件操作代码...printf("%d\n",fd);close(fd);return 0;
}

运行结果:

 解释一下这里为什么是3:

在Linux中我们都说一切皆文件,在此下

我们在C语言的文件操作中就已经知道在一个C语言的程序中,他会默认为我们打开3个流,分别是stdin ,stdout ,stderr,这三个就是对应文件表的下标0,1, 2,所以我们在打开也就是从3下标开始。

那么这时候就存在疑问了,那这三个流对应的是什么文件呢?其实也不难想到,在Linux下一切皆文件,那么在Linux下,键盘,显示器那它也应该是文件,这点如何理解呢?就好比,我们现在将显示器想象为一个文件,那么我们我们显示器现实一个图像,就可以理解为向显示器文件内输入一个图像。因为我们这个文件一直再打开,那么它就会立刻回显文件内容,那么我们也就可以看到显示器出现了一个图像。

在程序启动时,stdinstdoutstderr 默认被绑定到特定的设备文件:

但对于stdin ,stdout ,stderr,这三个流也有自己的名字。

  • stdin(标准输入流)
    • 默认绑定到 键盘,对应文件:/dev/stdin/dev/tty
    • 键盘输入被视为对文件内容的读取操作。
  • stdout(标准输出流)
    • 默认绑定到 终端(显示器),对应文件:/dev/stdout/dev/tty
    • 程序向标准输出写入内容,相当于向显示器“文件”写数据。
  • stderr(标准错误流)
    • 默认也绑定到 终端(显示器),对应文件:/dev/stderr/dev/tty
    • 用于输出错误信息,与 stdout 独立,便于区分正常输出和错误信息。

这些绑定允许程序与用户进行交互,而不需要关心具体设备。

注意:这三个流的类型都是FILE*。fopen的返回值都是文件指针。

文件对流的管理--文件描述符fd

从上面的了解便得知。从操作系统的角度来看,我们了解到操作系统对每个文件的管理是从 文件对流的管理。对流的管理通常通过一个数组这个数组就为fd_array数组来实现,每个数组元素对应一个文件描述符,代表一个打开的文件。我们可以通过文件描述符的下标对文件进行访问和管理。

通常,操作系统会为每个进程默认打开三个流,分别是:

  • 下标 0:标准输入(stdin),通常对应键盘输入。
  • 下标 1:标准输出(stdout),通常对应终端输出。
  • 下标 2:标准错误输出(stderr),也通常对应终端。

如果需要打开更多文件,操作系统会从该数组从下标0开始遍历找到一个数据为空的下标(也就是下标 3 )开始为新打开的文件分配文件描述符fd。

那么既然是从下标3开始对流的管理,那么对于操作系统来说打开流的

再说下面内容之前,我们先回忆一些前面的内容

  • 我们C语言中学的函数fopen,fclose,fread,fwrite......都是C标准库当中的函数,我们也称之为库函数。
  • 而对于open,close,read,write都是属于系统提供的接口,称之为系统调用函数。
  • 再回忆之前的操作系统的一个图(下图),也就对系统调用接口和库函数的关系一目了然。
  • 所以可以认为, f 系列的函数,都是对系统调用的封装,方便二次开发。

那么对于调用函数 fopen 所打开的文件,那么它也应该对操作系统来说也会有一个下标,也应该作为流一样被管理。

那么验证代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main()
{int fd1 = open("test.txt", O_RDONLY | O_CREAT, 0666);printf("fd1:%d\n", fd1);FILE *fp = fopen("log1.txt", "r");if (fp == NULL){perror("Error opening file");}else{int fd3 = fileno(fp);                 // 获取底层的文件描述符printf("File descriptor: %d\n", fd3); // 打印文件描述符}int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);printf("fd1:%d\n", fd2);fclose(fp);close(fd1);close(fd2);return 0;
}

 那么在Xshell上运行的结果如下:

也是符合我们的预期的。 

文件描述符的分配规则

当然如果我们每打开一个文件后然后再关闭,那么对于文件描述符fd它始终都会是3。又或者说如果我们先打开3,然后再打开一个4,然后先关闭3,然后再打开,那么这个新打开的就会分配文件描述符为3。

代码验证展示如下:

#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_RDONLY);if (fd1 < 0){perror("open");return 1;}printf("fd1:%d\n",fd1);close(fd1);int fd2 = open("log1.txt", O_RDONLY);if (fd2 < 0){perror("open");return 1;}printf("fd2:%d\n",fd2);close(fd2);fd1 = open("log.txt", O_RDONLY);fd2 = open("log1.txt", O_RDONLY);printf("fd1:%d\n",fd1);printf("fd2:%d\n",fd2);close(fd1);printf("关闭fd1后\n");int fd3 = open("log2.txt", O_RDONLY);printf("fd3:%d\n",fd3);close(fd2);close(fd3);return 0;
}

运行结果如下: 

结论: 文件描述符是从最小但是没有被使用的fd_array数组下标开始进行分配的。

write接口介绍

系统接口中使用write函数向文件写入信息,write函数的函数原型如下:

ssize_t write(int fd, const void *buf, size_t count);

我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。

  • 如果数据写入成功,实际写入数据的字节个数被返回。
  • 如果数据写入失败,-1被返回。

对文件进行写入操作示例:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}const char* msg = "hello Linux\n";for (int i = 0; i < 5; i++){write(fd, msg, strlen(msg));// 这里的strlen要不要+1?其实不需要// 因为虽然c语言规定字符串是以'\0'结尾,但是c语言的规定与文件无关,// 如果'\0'被写入文件则会变成乱码,所以可以写入,但是没必要也不需要。}close(fd);return 0;
}

运行程序后,在当前路径下就会生成对应文件,文件当中就是我们写入的内容。

 read接口介绍

系统接口中使用read函数从文件读取信息,read函数的函数原型如下:

ssize_t read(int fd, void *buf, size_t count);

我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。

  • 如果数据读取成功,实际读取数据的字节个数被返回。
  • 如果数据读取失败,-1被返回。

对文件进行读取操作示例:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("log.txt", O_RDONLY);if (fd < 0){perror("open");return 1;}char ch;while (1){ssize_t s = read(fd, &ch, 1);if (s <= 0){break;}write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器文件写入数据}close(fd);return 0;
}

运行程序后,就会将我们刚才写入文件的内容读取出来,并打印在显示器文件上。 

 重定向

重定向的原理

在明确了文件描述符的概念及其分配规则后,现在我们已经具备理解重定向原理的能力了。看完下面三个例子后,你会发现重定向的本质就是修改文件描述符下标对应的struct file*的内容。

输出重定向原理 >

输出重定向就是,将我们本应该输出到一个文件的数据重定向输出到另一个文件中。

就比如,我们调用函数printf,想本应该输出到“显示器文件”的数据输出到log.txt文件当中, 那么我们可以在打开log.txt文件之前将文件描述符为1的文件关闭,也就是将“显示器文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是1。

#include <stdio.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_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}printf("hello world\n");printf("hello world\n");printf("hello world\n");printf("hello world\n");printf("hello world\n");fflush(stdout);// 刷新缓冲区,这个后面会说明文件缓冲区close(fd);return 0;
}

运行结果后,我们发现显示器上并没有输出数据,对应数据输出到了log.txt文件当中。

说明一下:

  • printf函数是默认向stdout输出数据的,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。
  • C语言的数据并不是立马写到了内存操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。

追加重定向原理 >>

追加重定向和输出重定向的唯一区别就是,输出重定向是覆盖式输出数据,而追加重定向是追加式输出数据。

例如,如果我们想让本应该输出到“显示器文件”的数据追加式输出到log.txt文件当中,那么我们应该先将文件描述符为1的文件关闭,然后再以追加式写入的方式打开log.txt文件,这样一来,我们就将数据追加重定向到了文件log.txt当中。 

#include <stdio.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_WRONLY|O_APPEND|O_CREAT, 0666);if(fd < 0){perror("open");return 1;}printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");fflush(stdout);close(fd);return 0;
}

运行结果后,我们发现对应数据便追加式输出到了log.txt文件当中。 

输入重定向原理  <

输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据。 

例如,如果我们想让本应该从“键盘文件”读取数据的scanf函数,改为从log.txt文件当中读取数据,那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将“键盘文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0。

#include <stdio.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 | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}char buf[40];while (scanf("%s", buf) != EOF){printf("%s\n", buf);}close(fd);return 0;
}

 运行结果后,我们发现scanf函数将log.txt文件当中的数据都读取出来了。

说明一下:
scanf函数是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf实际上就是向文件描述符为0的文件读取数据。 

标准输出流和标准错误流对应的都是显示器,它们有什么区别?

    • 标准输出流(stdout:用于正常的程序输出信息。这些输出通常是程序的结果、状态信息等,用户希望看到的正常数据。
    • 标准错误流(stderr:用于输出错误信息或警告信息。这些输出通常是程序在执行过程中出现的错误、异常或不符合预期的状态。
  • 输出流的处理

    • 标准输出流:通常可以被重定向(例如,通过管道或将输出保存到文件中),这使得程序可以灵活地将结果传递到其他程序或保存。
    • 标准错误流:虽然也可以被重定向,但它通常保持与标准输出流分开,以便用户能够单独查看错误信息,而不被正常输出所干扰。

 下面我就验证一下这两条。

首先,我们来看看下面这段代码,代码中分别向标准输出流和标准错误流输出了两行字符串。

#include <stdio.h>
int main()
{printf("hello printf\n"); //stdoutperror("perror"); //stderrfprintf(stdout, "stdout:hello fprintf\n"); //stdoutfprintf(stderr, "stderr:hello fprintf\n"); //stderrreturn 0;
}

然后运行结果就是在显示器上输出四行字符串。 

将程序运行结果重定向输出到文件log.txt当中,我们会发现log.txt文件当中只有向标准输出流输出的两行字符串,而向标准错误流输出的两行数据并没有重定向到文件当中,而是仍然输出到了显示器上。

 dup2系统调用

重定向标准输出流(stdout)的核心思想就是将文件描述符从标准输出(文件描述符为 1)重定向到你所指定的文件。代表了不同的文件描述符。通过将文件描述符数组中的一个元素的内容(比如 fd_array[3])拷贝到标准输出流的文件描述符位置(即 fd_array[1]),可以实现输出重定向。

在 C 语言中,文件描述符是操作系统用来管理文件的整数值,1 代表标准输出(stdout),2 代表标准错误输出(stderr)。所以,重定向的实质是将标准输出流的文件描述符从其默认的终端位置修改为新的文件描述符,从而将输出写入文件。

当然重定向也可以进行fd_array数组当中元素的拷贝即可。

比如下图所示:

在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。dup2的函数原型如下: 

int dup2(int oldfd, int newfd);

 函数功能: dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,如果有必要的话我们需要先使用关闭文件描述符为newfd的文件。注意是将oldfd的内容拷贝到newfd中!!!

函数返回值: dup2如果调用成功,返回newfd,否则返回-1。

使用dup2时,我们需要注意以下两点:

  1. 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
  2. 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。

例如,我们将打开文件log.txt时获取到的文件描述符和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件当中。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}close(1);dup2(fd, 1);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0;
}

运行也可以看到数据被输出到了log.txt文件当中。

添加重定向功能到minishell 

对获取的命令进行解析时,如果命令包含重定向符号 >>><,则需要进行相应的处理。具体步骤如下:

  1. 设置一个变量 type 用于标识重定向类型:

    • type = 0 表示命令包含输出重定向符号 >
    • type = 1 表示命令包含追加重定向符号 >>
    • type = 2 表示命令包含输入重定向符号 <
  2. 解析重定向符号后的字段,将其视为目标文件名。

    • 如果 type = 0,则以写入模式打开目标文件(覆盖文件内容)。
    • 如果 type = 1,则以追加模式打开目标文件(在文件末尾追加内容)。
    • 如果 type = 2,则以读取模式打开目标文件。
  3. 根据 type 的值使用 dup2 接口实现重定向:

    • type = 0type = 1 时,通过 dup2 将目标文件描述符与标准输出流(文件描述符 STDOUT_FILENO)重定向。
    • type = 2 时,通过 dup2 将目标文件描述符与标准输入流(文件描述符 STDIN_FILENO)重定向。

这种处理方式确保命令可以正确地实现输入/输出的重定向,同时保持代码逻辑清晰和易于维护。

代码实现如下:

#include <stdio.h>
#include <fcntl.h>
#include <ctype.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEN 1024 // 命令最大长度
#define NUM 32   // 命令拆分后的最大个数
int main()
{int type = 0;      // 0 >   1 >>    2 <char cmd[LEN];     // 存储命令char *myargv[NUM]; // 存储命令拆分后的结果char hostname[32]; // 主机名char pwd[128];     // 当前目录while (1){// 获取命令提示信息  [用户名+主机名+当前目录]struct passwd *pass = getpwuid(getuid()); // 获取该用户的密码文件记录(passwd 结构)。它通常用于查找用户信息,如用户名、主目录路径和登录 shell 等。gethostname(hostname, sizeof(hostname) - 1);getcwd(pwd, sizeof(pwd) - 1);int len = strlen(pwd);char *p = pwd + len - 1;while (*p != '/'){p--;}p++;// 打印命令提示信息printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);// 读取命令fgets(cmd, LEN, stdin);cmd[strlen(cmd) - 1] = '\0';// 实现重定向功能char *start = cmd;while (*start != '\0'){if (*start == '>'){type = 0; // 遇到一个'>',输出重定向*start = '\0';start++;if (*start == '>'){type = 1; // 遇到第二个'>',追加重定向start++;}break;}if (*start == '<'){type = 2;*start = '\0';start++;break;}start++;}if (*start != '\0'){// start位置不为'\0',说明命令包含重定向内容while (isspace(*start)) // 跳过重定向符号后面的空格start++;}else{start = NULL; // start设置为NULL,标识命令当中不含重定向内容}// 拆分命令myargv[0] = strtok(cmd, " ");int i = 1;while (myargv[i] = strtok(NULL, " ")){i++;}pid_t id = fork(); // 创建子进程执行命令if (id == 0){// childif (start != NULL){if (type == 0){                                                             // 输出重定向int fd = open(start, O_WRONLY | O_CREAT | O_TRUNC, 0664); // 以写的方式打开文件(清空原文件内容)if (fd < 0){perror("open");exit(2);}close(1);dup2(fd, 1); // 重定向}else if (type == 1){                                                              // 追加重定向int fd = open(start, O_WRONLY | O_APPEND | O_CREAT, 0664); // 以追加的方式打开文件if (fd < 0){perror("open");exit(2);}close(1);dup2(fd, 1); // 重定向}else{                                   // 输入重定向int fd = open(start, O_RDONLY); // 以读的方式打开文件if (fd < 0){perror("open");exit(2);}close(0);dup2(fd, 0); // 重定向}}execvp(myargv[0], myargv); // child进行程序替换exit(1);                   // 替换失败的退出码设置为1}// shellint status = 0;pid_t ret = waitpid(id, &status, 0); // shell等待child退出if (ret > 0){printf("exit code:%d\n", WEXITSTATUS(status)); // 打印child的退出码}}return 0;
}

效果截图: 

但是这时候就不免有疑惑,虽然我们对我们的minishell中做了重定向处理,但为什么对子进程进行进程替换后,代码执行结果不受影响呢?

子进程和程序替换的原理

  1. 子进程的生成
    minishell 中,通常使用 fork() 创建子进程。fork() 的作用是复制父进程的地址空间,子进程在此基础上独立运行。

  2. 程序替换
    子进程生成后,通常会调用 exec() 系列函数进行程序替换(例如 execve())。

    • 程序替换的作用exec() 系列函数会用新的可执行程序替换当前进程的代码段和数据段,同时重置相关的用户态数据(如堆栈等)。
    • 程序替换的特点:虽然代码段和数据段被替换,但文件描述符等与内核相关的资源不会改变,依然保持原有状态。
  3. 文件描述符的继承
    文件描述符是由内核管理的资源,存储在进程的文件描述符表中。而 exec() 调用只替换用户态的数据和代码,不会影响文件描述符的状态。

    • fork() 后,子进程继承了父进程的文件描述符表。
    • 如果父进程在 fork() 后对文件描述符进行了重定向(如 dup2()),这种重定向对子进程同样生效。

为什么重定向后不会受影响?

当我们在 minishell 中对子进程做重定向处理后:

  1. 子进程的文件描述符被重定向到了指定的文件或管道。
  2. 调用 exec() 进行程序替换时,文件描述符仍然保持重定向的状态,因为它们是内核资源,不会受程序替换影响。

http://www.mrgr.cn/news/83636.html

相关文章:

  • Unity 人体切片三维可视化,可任意裁切切割。查看不同断层的图像。
  • Linux网络编程4——系统编程
  • ros2笔记-6.2 使用urdf创建机器人模型
  • 一个简单的html5导航页面
  • OpenCV基于均值漂移算法(pyrMeanShiftFiltering)的水彩画特效
  • 实时数仓:基于数据湖的实时数仓与数据治理架构
  • SpringBoot之LazyInitializationBeanFactoryPostProcessor类源码学习
  • 金融项目实战 04|JMeter实现自动化脚本接口测试及持续集成
  • LLMBook 大模型数据集下载地址完整收集
  • Github配置ssh key,密钥配对错误怎么解决?
  • Open FPV VTX开源之第一次出图
  • Day05-后端Web基础——TomcatServletHTTP协议SpringBootWeb入门
  • 【AIDD药物研发】张载熙-生成式AI4药物发现
  • 《自动驾驶与机器人中的SLAM技术》ch8:基于 IESKF 的紧耦合 LIO 系统
  • 知识图谱抽取分析中,如何做好实体对齐?
  • Java List去重:Stream、HashMap与TreeSet对比分析
  • 初识Java3
  • 前端验证码实现
  • Java线程池解密
  • SpringData-Redis缓存之RedisTemplate
  • SpringCloud微服务:基于Nacos组件,整合Dubbo框架
  • 镀锡薄板与镀锡废水详解
  • 中等难度——python实现电子宠物和截图工具
  • 宁德时代C++后端开发面试题及参考答案
  • 51单片机——I2C-EEPROM
  • 【0388】Postgres内核 SQL function 删除 physical replication slot 底层实现( 4 )