Linux的基础IO

简介: 那么,这里我们温习一下操作系统的概念我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。这就是因为在根本上操作系统确实像银行一样,并不完全信任用户程序,因为直接开放底层资源(如内存、磁盘、硬件访问权限)给用户程序会带来巨大的风险。所以就向银行一样他的服务是由工作人员隔着一层玻璃,然后对顾客进行服务的。


目录

先来段代码回顾C文件接口
test.c向文件写入

include

include

include

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

include

include

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

include

include

include

include

include

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

include

include

include

include

include

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));//类比write
    if(s > 0){
        printf("%s",buf);
    }
    else{
        break;
    }
}
close(fd);
return 0;

}
在Xshell下运行结果:

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

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

指令:

man open
下面就由我来介绍一下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
注意点:

第一个参数

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

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

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

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

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

若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。
umask(0); //将文件默认掩码设置为0
对于返回值的介绍我放在下面 的从操作系统视角来看文件管理 部分。

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

就比如下面的一段代码:

include

include

include

int main()
{
int fd = open("test.txt", O_RDONLY);
// 文件操作代码...
close(fd);
return 0;
}
从用户的角度来看,那他就简简单单的调用了open接口,然后就进行一些文件操作,然后再关闭文件,但是在操作系统的角度来看,其实并没有那么简单,其实是做了一系列复杂的操作。

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

文件描述符 是一个非负整数,它用于标识和管理打开的文件。
操作系统会维护一个文件描述符表,用于跟踪每个文件描述符所对应的文件资源(例如,文件的读写指针、文件类型、文件系统上的实际位置等)。
当用户调用open接口的时候,操作系统从 文件描述符表 中分配一个空闲的描述符。如果该 文件描述符表 已经被占用,那么操作系统就会下一个可用的文件描述符开始分配。如果有感觉的话,其实这个过程很像遍历一个数组,从下标0开始遍历,直到找到一个空的空间然后分配数据。(其实这个管理 文件描述符表 的确是是一个数组。这个数组就叫文件表)

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

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

在 内核空间 中打开文件。操作系统会通过文件路径定位文件,并检查文件是否存在。如果文件不存在,并且用户指定了 O_CREAT 标志,操作系统会创建文件。
更新文件系统的文件表。操作系统会在内部维护一个文件表(文件系统表)来跟踪文件的状态。这包括文件的读写位置、访问模式、权限等信息。
到这里才真正的打开了文件。并且还可以通过文件表的下标进行维护。(操作系统还会做一些一些别的操作,但到这里已经打开了文件,下面的一些操作我们这里并不关心,就不说了)

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

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

include

include

include

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下,键盘,显示器那它也应该是文件,这点如何理解呢?就好比,我们现在将显示器想象为一个文件,那么我们我们显示器现实一个图像,就可以理解为向显示器文件内输入一个图像。因为我们这个文件一直再打开,那么它就会立刻回显文件内容,那么我们也就可以看到显示器出现了一个图像。

在程序启动时,stdin、stdout 和 stderr 默认被绑定到特定的设备文件:

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

stdin(标准输入流):
默认绑定到 键盘,对应文件:/dev/stdin 或 /dev/tty
键盘输入被视为对文件内容的读取操作。
stdout(标准输出流):
默认绑定到 终端(显示器),对应文件:/dev/stdout 或 /dev/tty
程序向标准输出写入内容,相当于向显示器“文件”写数据。
stderr(标准错误流):
默认也绑定到 终端(显示器),对应文件:/dev/stderr 或 /dev/tty
用于输出错误信息,与 stdout 独立,便于区分正常输出和错误信息。
这些绑定允许程序与用户进行交互,而不需要关心具体设备。

文件对流的管理--文件描述符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

include

include

include

include

include

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

include

include

include

include

include

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;
}
运行结果如下:

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

ssize_t write(int fd, const void *buf, size_t count);
我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。

如果数据写入成功,实际写入数据的字节个数被返回。
如果数据写入失败,-1被返回。
对文件进行写入操作示例:

include

include

include

include

include

include

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

include

include

include

include

include

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

include

include

include

include

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

include

include

include

include

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

include

include

include

include

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

int main()
{
printf("hello printf\n"); //stdout
perror("perror"); //stderr

fprintf(stdout, "stdout:hello fprintf\n"); //stdout
fprintf(stderr, "stderr:hello fprintf\n"); //stderr
return 0;

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

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

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

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

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

比如下图所示:

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

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

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

如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。
例如,我们将打开文件log.txt时获取到的文件描述符和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件当中。

include

include

include

include

include

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

type = 0 表示命令包含输出重定向符号 >。
type = 1 表示命令包含追加重定向符号 >>。
type = 2 表示命令包含输入重定向符号 <。

如果 type = 0,则以写入模式打开目标文件(覆盖文件内容)。
如果 type = 1,则以追加模式打开目标文件(在文件末尾追加内容)。
如果 type = 2,则以读取模式打开目标文件。

当 type = 0 或 type = 1 时,通过 dup2 将目标文件描述符与标准输出流(文件描述符 STDOUT_FILENO)重定向。
当 type = 2 时,通过 dup2 将目标文件描述符与标准输入流(文件描述符 STDIN_FILENO)重定向。
这种处理方式确保命令可以正确地实现输入/输出的重定向,同时保持代码逻辑清晰和易于维护。

include

include

include

include

include

include

include

include

include

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)
    {
        // child
        if (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
    }
    // shell
    int 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中做了重定向处理,但为什么对子进程进行进程替换后,代码执行结果不受影响呢?

子进程和程序替换的原理

程序替换的作用:exec() 系列函数会用新的可执行程序替换当前进程的代码段和数据段,同时重置相关的用户态数据(如堆栈等)。
程序替换的特点:虽然代码段和数据段被替换,但文件描述符等与内核相关的资源不会改变,依然保持原有状态。

在 fork() 后,子进程继承了父进程的文件描述符表。
如果父进程在 fork() 后对文件描述符进行了重定向(如 dup2()),这种重定向对子进程同样生效。
为什么重定向后不会受影响?
当我们在 minishell 中对子进程做重定向处理后:

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

目录
相关文章
|
9月前
|
网络协议 安全 Linux
Linux C/C++之IO多路复用(select)
这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。
215 0
|
1月前
|
Linux C语言 网络架构
Linux的基础IO内容补充-FILE
而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fwrite函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和fwrite函数打印的数据就有两份。此时我们就可以知道,
35 0
|
9月前
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
363 1
Linux C/C++之IO多路复用(aio)
|
7月前
|
Ubuntu Linux Shell
(已解决)Linux环境—bash: wget: command not found; Docker pull报错Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled
(已成功解决)Linux环境报错—bash: wget: command not found;常见Linux发行版本,Linux中yum、rpm、apt-get、wget的区别;Docker pull报错Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled
2723 68
(已解决)Linux环境—bash: wget: command not found; Docker pull报错Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled
|
5月前
|
存储 网络协议 Linux
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
220 34
|
11月前
|
缓存 安全 Linux
Linux 五种IO模型
Linux 五种IO模型
|
7月前
|
Linux API C语言
Linux基础IO
Linux基础IO操作是系统管理和开发的基本技能。通过掌握文件描述符、重定向与管道、性能分析工具、文件系统操作以及网络IO命令等内容,可以更高效地进行系统操作和脚本编写。希望本文提供的知识和示例能帮助读者更深入地理解和运用Linux IO操作。
143 14
|
9月前
|
Linux C++
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
231 0
Linux C/C++之IO多路复用(poll,epoll)
|
Linux 数据处理 C语言
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
153 0
|
11月前
|
小程序 Linux 开发者
Linux之缓冲区与C库IO函数简单模拟
通过上述编程实例,可以对Linux系统中缓冲区和C库IO函数如何提高文件读写效率有了一个基本的了解。开发者需要根据应用程序的具体需求来选择合适的IO策略。
92 0